@cocorograph/hub-agent 0.6.47 → 0.6.49
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/claude-history.mjs +34 -0
- package/src/main.mjs +65 -2
- package/src/tui-permission-bridge.mjs +11 -2
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -241,3 +241,37 @@ export async function listSessions({ cwd, projectsRoot, limit = 30, logger }) {
|
|
|
241
241
|
}
|
|
242
242
|
return { sessions }
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* TUI チャット閲覧中に裏の対話 claude が新しい session_id へ「回転」したか
|
|
247
|
+
* (= `/clear` で新 jsonl が生まれたか) を判定する純関数。
|
|
248
|
+
*
|
|
249
|
+
* 背景: TUI チャットは bind 時に決めた session_id を固定して jsonl を読む。ユーザーが
|
|
250
|
+
* `/clear` すると裏の claude は**新しい session_id (新 jsonl)** に切り替わるが、フロントは
|
|
251
|
+
* 旧 jsonl を読み続けるため「読み出し先が切り替わらない」「生成中で固まる」「新セッションへの
|
|
252
|
+
* 送信が表示されない」という不整合が起きる (T04786 後続調査)。これを agent 側で検知して
|
|
253
|
+
* `claude.session.rotated` を push し、フロントに読み出し先の切替を促すための判定ロジック。
|
|
254
|
+
*
|
|
255
|
+
* 判定材料:
|
|
256
|
+
* - `viewingSessionId`: フロントが今表示している (閲覧ハートビートが運ぶ) session_id。
|
|
257
|
+
* - `newestSessionId`: cwd 配下で実際に最新 (mtime 降順先頭) の session_id。
|
|
258
|
+
* - `lastNotifiedNewId`: 同一 new への多重通知を防ぐため、最後に通知した new_session_id。
|
|
259
|
+
*
|
|
260
|
+
* 回転とみなす条件: 最新が存在し、閲覧中 id と異なり、かつ直近で同じ new を通知済みでない。
|
|
261
|
+
* 注意: 過去セッションを意図的に開いている (= newest 非追従) ビューでは呼び出し側が
|
|
262
|
+
* `follow_newest=false` でこの判定自体をスキップすること (ピン留め閲覧を勝手に最新へ
|
|
263
|
+
* 引きずらないため)。本関数は「追従中ビュー」前提で newest とのズレだけを見る。
|
|
264
|
+
*
|
|
265
|
+
* @param {{viewingSessionId?: string|null, newestSessionId?: string|null, lastNotifiedNewId?: string|null}} args
|
|
266
|
+
* @returns {{rotated: boolean, newSessionId?: string}}
|
|
267
|
+
*/
|
|
268
|
+
export function decideSessionRotation({
|
|
269
|
+
viewingSessionId,
|
|
270
|
+
newestSessionId,
|
|
271
|
+
lastNotifiedNewId,
|
|
272
|
+
} = {}) {
|
|
273
|
+
if (!newestSessionId || !viewingSessionId) return { rotated: false }
|
|
274
|
+
if (newestSessionId === viewingSessionId) return { rotated: false }
|
|
275
|
+
if (newestSessionId === lastNotifiedNewId) return { rotated: false }
|
|
276
|
+
return { rotated: true, newSessionId: newestSessionId }
|
|
277
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -24,7 +24,11 @@ import { WsClient } from "./ws-client.mjs"
|
|
|
24
24
|
import { PtyBridge } from "./pty-bridge.mjs"
|
|
25
25
|
import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
|
|
26
26
|
import { UploadManager } from "./claude-upload.mjs"
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
decideSessionRotation,
|
|
29
|
+
fetchSessionHistory,
|
|
30
|
+
listSessions,
|
|
31
|
+
} from "./claude-history.mjs"
|
|
28
32
|
import { listAgents } from "./agents.mjs"
|
|
29
33
|
import { listSkills } from "./skills.mjs"
|
|
30
34
|
import { listSessionStates } from "./state.mjs"
|
|
@@ -457,6 +461,12 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
457
461
|
// 同じ id への再 respawn (= claude 再起動) を抑止する。
|
|
458
462
|
ctx.tuiReboundSessions = new Map()
|
|
459
463
|
|
|
464
|
+
// session 回転通知 (T04786 後続) の多重送信ガード: 回転キー (tmux 名 or cwd) →
|
|
465
|
+
// 最後に push した new_session_id。閲覧ハートビートは 5s 間隔で来るため、フロントが
|
|
466
|
+
// 切替を完了し new id でハートビートを送り直すまでの間に来る余分なハートビートで
|
|
467
|
+
// claude.session.rotated を連発しないようにする。
|
|
468
|
+
ctx.tuiRotationNotified = new Map()
|
|
469
|
+
|
|
460
470
|
const shutdown = async (signal) => {
|
|
461
471
|
logger.info({ signal }, "shutting down")
|
|
462
472
|
await runHookBroadcast(plugins, "onAgentStop", ctx)
|
|
@@ -1028,6 +1038,7 @@ async function dispatch(msg, ctx) {
|
|
|
1028
1038
|
msg.request_id,
|
|
1029
1039
|
!!msg.allow,
|
|
1030
1040
|
msg.deny_message,
|
|
1041
|
+
msg.updated_input,
|
|
1031
1042
|
)
|
|
1032
1043
|
: false
|
|
1033
1044
|
if (handledByTui) return
|
|
@@ -1045,7 +1056,7 @@ async function dispatch(msg, ctx) {
|
|
|
1045
1056
|
if (!ctx.claudeBridge) return
|
|
1046
1057
|
ctx.claudeBridge.interrupt({ stream_id: msg.stream_id })
|
|
1047
1058
|
return
|
|
1048
|
-
case "claude.tui.viewing":
|
|
1059
|
+
case "claude.tui.viewing": {
|
|
1049
1060
|
// T04778 watcher ゲート: browser が TUI チャットでこの session を閲覧中。
|
|
1050
1061
|
// 閲覧マーカーを更新し、承認フックが「閲覧者あり」と判定できるようにする。
|
|
1051
1062
|
ctx.tuiViewerRegistry
|
|
@@ -1055,7 +1066,59 @@ async function dispatch(msg, ctx) {
|
|
|
1055
1066
|
ctx.jsonlLiveWatchers
|
|
1056
1067
|
?.note({ session_id: msg.session_id, cwd: msg.cwd })
|
|
1057
1068
|
.catch(() => {})
|
|
1069
|
+
// T04786 後続: session 回転検知 (/clear で裏 claude が新 session_id へ切替)。
|
|
1070
|
+
// 追従中ビュー (follow_newest 既定 true) のときだけ、cwd の最新 session_id と
|
|
1071
|
+
// 閲覧中 id を突き合わせ、ズレていれば claude.session.rotated を push してフロントに
|
|
1072
|
+
// 読み出し先の切替を促す。過去セッションを意図的に開いているビュー (follow_newest=false)
|
|
1073
|
+
// では検知しない (ピン留め閲覧を最新へ引きずらない)。
|
|
1074
|
+
const viewSid = msg.session_id
|
|
1075
|
+
const viewCwd = msg.cwd
|
|
1076
|
+
const viewName =
|
|
1077
|
+
typeof msg.session_name === "string" ? msg.session_name : ""
|
|
1078
|
+
const followNewest = msg.follow_newest !== false
|
|
1079
|
+
if (followNewest && viewSid && viewCwd) {
|
|
1080
|
+
;(async () => {
|
|
1081
|
+
try {
|
|
1082
|
+
const projectsRoot = await getActiveProjectsRoot()
|
|
1083
|
+
const { sessions } = await listSessions({
|
|
1084
|
+
cwd: viewCwd,
|
|
1085
|
+
projectsRoot,
|
|
1086
|
+
limit: 1,
|
|
1087
|
+
logger,
|
|
1088
|
+
})
|
|
1089
|
+
const newestId = sessions?.[0]?.session_id || null
|
|
1090
|
+
const key = viewName || viewCwd
|
|
1091
|
+
const { rotated, newSessionId } = decideSessionRotation({
|
|
1092
|
+
viewingSessionId: viewSid,
|
|
1093
|
+
newestSessionId: newestId,
|
|
1094
|
+
lastNotifiedNewId: ctx.tuiRotationNotified.get(key),
|
|
1095
|
+
})
|
|
1096
|
+
if (!rotated) return
|
|
1097
|
+
ctx.tuiRotationNotified.set(key, newSessionId)
|
|
1098
|
+
// 冪等マップも新 id へ更新し、将来の remount bind が旧 id へ
|
|
1099
|
+
// respawn (claude 再起動) してしまうのを防ぐ。
|
|
1100
|
+
if (viewName) ctx.tuiReboundSessions.set(viewName, newSessionId)
|
|
1101
|
+
ctx.client.send({
|
|
1102
|
+
type: "claude.session.rotated",
|
|
1103
|
+
cwd: viewCwd,
|
|
1104
|
+
session_name: viewName || undefined,
|
|
1105
|
+
old_session_id: viewSid,
|
|
1106
|
+
new_session_id: newSessionId,
|
|
1107
|
+
})
|
|
1108
|
+
logger.info(
|
|
1109
|
+
{ session: viewName, cwd: viewCwd, old: viewSid, new: newSessionId },
|
|
1110
|
+
"tui session rotated (/clear) → notified browser",
|
|
1111
|
+
)
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
logger.warn(
|
|
1114
|
+
{ err: err?.message, cwd: viewCwd },
|
|
1115
|
+
"tui session rotation check failed",
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
})()
|
|
1119
|
+
}
|
|
1058
1120
|
return
|
|
1121
|
+
}
|
|
1059
1122
|
case "claude.tui.unviewing":
|
|
1060
1123
|
// 閲覧終了。session_id マーカーを即失効させ、以降の Bash を即 ask に倒す。
|
|
1061
1124
|
ctx.tuiViewerRegistry
|
|
@@ -114,9 +114,12 @@ export class TuiPermissionBridge extends EventEmitter {
|
|
|
114
114
|
* @param {string} request_id
|
|
115
115
|
* @param {boolean} allow
|
|
116
116
|
* @param {string} [denyMessage]
|
|
117
|
+
* @param {unknown} [updatedInput] AskUserQuestion 等の回答 ({questions, answers})。
|
|
118
|
+
* フックが PreToolUse の hookSpecificOutput.updatedInput として注入し、claude が
|
|
119
|
+
* 対話 UI を出さず確定する。Bash 等の allow/deny だけで済むツールでは undefined。
|
|
117
120
|
* @returns {Promise<boolean>} この bridge の管理下だったか
|
|
118
121
|
*/
|
|
119
|
-
async resolve(request_id, allow, denyMessage) {
|
|
122
|
+
async resolve(request_id, allow, denyMessage, updatedInput) {
|
|
120
123
|
if (!this._pending.has(request_id)) return false
|
|
121
124
|
this._pending.delete(request_id)
|
|
122
125
|
const decision = allow ? "allow" : "deny"
|
|
@@ -125,7 +128,13 @@ export class TuiPermissionBridge extends EventEmitter {
|
|
|
125
128
|
try {
|
|
126
129
|
await writeFile(
|
|
127
130
|
tmp,
|
|
128
|
-
JSON.stringify({
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
decision,
|
|
133
|
+
deny_message: denyMessage ?? null,
|
|
134
|
+
// 回答 (AskUserQuestion の {questions, answers} 等)。フックが
|
|
135
|
+
// updatedInput として注入する。無ければ null (Bash 等)。
|
|
136
|
+
updated_input: updatedInput ?? null,
|
|
137
|
+
}),
|
|
129
138
|
)
|
|
130
139
|
await rename(tmp, fp)
|
|
131
140
|
} catch (err) {
|