@cocorograph/hub-agent 0.8.75 → 0.8.77
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/async-ttl-cache.mjs +25 -11
- package/src/chat-removal-guard.mjs +62 -0
- package/src/chat-session-starter.mjs +90 -0
- package/src/chat-sessions-snapshot.mjs +9 -0
- package/src/chat-targets.mjs +60 -0
- package/src/chat-workspace.mjs +279 -0
- package/src/claude-stream-bridge.mjs +13 -5
- package/src/codex-stream-bridge.mjs +59 -7
- package/src/dispatch-scheduler.mjs +45 -5
- package/src/main.mjs +291 -17
- package/src/review-event-store.mjs +141 -0
- package/src/sibling-inject.mjs +90 -5
- package/src/tmux.mjs +53 -174
|
@@ -43,8 +43,9 @@
|
|
|
43
43
|
import { EventEmitter } from "node:events"
|
|
44
44
|
import { randomUUID } from "node:crypto"
|
|
45
45
|
import path from "node:path"
|
|
46
|
+
import { isUnderRemoval } from "./chat-removal-guard.mjs"
|
|
46
47
|
|
|
47
|
-
import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
|
|
48
|
+
import { lookupInjectSource, recordInjectSourcePersistent } from "./inject-source-store.mjs"
|
|
48
49
|
|
|
49
50
|
import { AsyncTtlCache } from "./async-ttl-cache.mjs"
|
|
50
51
|
import { CodexAppServerClient } from "./codex-appserver-client.mjs"
|
|
@@ -203,6 +204,8 @@ function siblingEnvFor(cwd) {
|
|
|
203
204
|
...process.env,
|
|
204
205
|
COCKPIT_SIBLING_TOKEN: getOrCreateSiblingToken(cwd),
|
|
205
206
|
COCKPIT_SIBLING_CWD: cwd,
|
|
207
|
+
// Spawn-time display capability; rejection by an older daemon does not stop the review.
|
|
208
|
+
COCKPIT_REVIEW_EVENTS: "1",
|
|
206
209
|
// ask-sibling スキル (cockpit_orch.py) が「自分は claude/codex どちらか」を
|
|
207
210
|
// 環境変数から確実に判定できるようにする (ヒューリスティック不要)。
|
|
208
211
|
COCKPIT_SIBLING_SELF_KIND: "codex",
|
|
@@ -798,6 +801,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
798
801
|
if (this.sessions.has(stream_id)) {
|
|
799
802
|
throw new Error(`stream_id already attached: ${stream_id}`)
|
|
800
803
|
}
|
|
804
|
+
if (isUnderRemoval(cwd)) throw new Error(`worktree removal in progress: ${cwd}`)
|
|
801
805
|
// 計測 (2026-07-31): attach 1 回 = 1 行の内訳ログ (`codex attach timing`)。
|
|
802
806
|
// frontend の attach_sent→ready_received は 1 つの数字でしか見えず、その中身が
|
|
803
807
|
// 「app-server の cold spawn + initialize」なのか「thread/resume の rollout 読み込み」なのか、
|
|
@@ -921,6 +925,11 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
921
925
|
}
|
|
922
926
|
}
|
|
923
927
|
|
|
928
|
+
// ⚠️ attach の入口でも同じ検査をしているが、そこから **_tryReattach の await** を挟んで
|
|
929
|
+
// ここへ来る(既存 session の thread 確定待ち)。その間に削除が始まっていることがあるので、
|
|
930
|
+
// プロセスを起こす直前にもう一度見る。ここを外すと「停止対象を列挙し終えたあとに新しい
|
|
931
|
+
// プロセスが生まれる」経路が残り、削除された cwd を指す会話ができる。
|
|
932
|
+
if (isUnderRemoval(cwd)) throw new Error(`worktree removal in progress: ${cwd}`)
|
|
924
933
|
const client = this._clientFactory({
|
|
925
934
|
cwd,
|
|
926
935
|
codexCmd: this.codexCmd,
|
|
@@ -1206,13 +1215,13 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1206
1215
|
: { items: [], start: 0, truncated: false, total: 0 }
|
|
1207
1216
|
// hub-agent 側の履歴整形コスト (巨大スレッドで効く)。thread_op_ms とは別勘定にする。
|
|
1208
1217
|
timing.history_flatten_ms = lap()
|
|
1209
|
-
const history = flat.items
|
|
1218
|
+
const history = await this._restoreHistoryInjectSources(flat.items, cwd)
|
|
1210
1219
|
const historyTruncated = flat.truncated
|
|
1211
1220
|
// T06541 案1: cold attach で得た高忠実度履歴をセッションに保持する。以降ライブ item
|
|
1212
1221
|
// 完了ごとに追記し、cwd reattach 時にこれを返す(新プロセス thread/resume を再実行しない)。
|
|
1213
1222
|
// major#2: byte 予算判定を O(1) にするため概算 byte 合計も持つ (flatten は既に byte 上限適用済み)。
|
|
1214
1223
|
// app-server reap 後の cold resume は rollout が正本になるため、未 rollout の空 fresh session や
|
|
1215
|
-
//
|
|
1224
|
+
// 未保存の sibling source タグは復元できない。空 session の滞留を避けるため fresh 例外は作らない。
|
|
1216
1225
|
session.historyItems = history.slice()
|
|
1217
1226
|
session.historyBytes = history.reduce((n, it) => n + estimateItemBytes(it), 0)
|
|
1218
1227
|
session.historyTruncated = historyTruncated
|
|
@@ -1800,7 +1809,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1800
1809
|
})
|
|
1801
1810
|
return {
|
|
1802
1811
|
threadId: session.threadId,
|
|
1803
|
-
history: page.items,
|
|
1812
|
+
history: await this._restoreHistoryInjectSources(page.items, session.cwd),
|
|
1804
1813
|
historyStart: page.start,
|
|
1805
1814
|
historyTotal: page.total,
|
|
1806
1815
|
historyTruncated: page.truncated,
|
|
@@ -1972,6 +1981,14 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1972
1981
|
return addCodexSessionTitles(sessions, sessionsDir)
|
|
1973
1982
|
}
|
|
1974
1983
|
|
|
1984
|
+
/** 指定 stream の live 会話を停止する。履歴ファイルは削除しない。 */
|
|
1985
|
+
stopSessionForStream({ stream_id, reason }) {
|
|
1986
|
+
const session = this.sessions.get(stream_id)
|
|
1987
|
+
if (!session || session.dead) return 0
|
|
1988
|
+
this._reapSession(session, Date.now(), reason || "explicit-stop")
|
|
1989
|
+
return 1
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1975
1992
|
async operateSession(options) {
|
|
1976
1993
|
const invalid = validateCodexSessionOperation(options)
|
|
1977
1994
|
if (invalid) return { ok: false, error: invalid }
|
|
@@ -2398,6 +2415,21 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2398
2415
|
this._settleInjectDelivery(handle, { delivered: false, reason: "input_failed" })
|
|
2399
2416
|
}
|
|
2400
2417
|
|
|
2418
|
+
/** 履歴にも既存の cwd・本文一致ストアから注入元を戻す。元 item と既存 source は保持する。 */
|
|
2419
|
+
async _restoreHistoryInjectSources(items, cwd) {
|
|
2420
|
+
return Promise.all(items.map(async (item) => {
|
|
2421
|
+
if (item?.type !== "userMessage" || item.source != null) return item
|
|
2422
|
+
const content = item.content
|
|
2423
|
+
const text = typeof content === "string"
|
|
2424
|
+
? content
|
|
2425
|
+
: Array.isArray(content)
|
|
2426
|
+
? content.map((block) => typeof block?.text === "string" ? block.text : "").join("")
|
|
2427
|
+
: ""
|
|
2428
|
+
const source = await lookupInjectSource(cwd, text)
|
|
2429
|
+
return source ? { ...item, source } : item
|
|
2430
|
+
}))
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2401
2433
|
/** userMessage の echo (item/completed) が控えた注入本文と一致すれば、params を **クローン**して
|
|
2402
2434
|
* item.source を付けたものを返す (元の app-server オブジェクトは変更しない)。一致しなければ null。
|
|
2403
2435
|
* frontend の userMessageItemToEvent がこの item.source を読み専用カードへ分岐する。 */
|
|
@@ -3306,8 +3338,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3306
3338
|
// ライブ emit だけタグしていたため、cwd reattach で historyItems が返ると連携カードが通常の青い
|
|
3307
3339
|
// 自分バブルへ戻っていた。同一プロセス内 reattach で表示が維持されるよう、以降 effectiveParams
|
|
3308
3340
|
// (タグ済み or 素) を history と emit の両方で使う。一致しなければ null → 素の params のまま。
|
|
3309
|
-
//
|
|
3310
|
-
// jsonl at-rest と同じ許容範囲。source 永続マップは TTL/ID 再利用の複雑さに見合わないため不採用)。
|
|
3341
|
+
// cold resume と過去履歴 page は既存の永続ストアから source を復元する。
|
|
3311
3342
|
const eventThreadId = msg?.params?.threadId
|
|
3312
3343
|
if (
|
|
3313
3344
|
typeof eventThreadId === "string" &&
|
|
@@ -4136,6 +4167,22 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4136
4167
|
return out
|
|
4137
4168
|
}
|
|
4138
4169
|
|
|
4170
|
+
/** Display events never start a turn and only target an exact thread in this cwd. */
|
|
4171
|
+
emitToStream(cwd, sessionId, event) {
|
|
4172
|
+
if (!cwd || !sessionId) return { status: "not_found" }
|
|
4173
|
+
const matches = [...this._liveSessions()].filter((s) => s.cwd === cwd && s.threadId === sessionId)
|
|
4174
|
+
if (matches.length !== 1) return { status: matches.length ? "ambiguous" : "not_found" }
|
|
4175
|
+
const session = matches[0]
|
|
4176
|
+
this.emit("event", {
|
|
4177
|
+
stream_id: session.stream_id,
|
|
4178
|
+
producer_seq: session.seq,
|
|
4179
|
+
thread_id: session.threadId,
|
|
4180
|
+
cwd: session.cwd,
|
|
4181
|
+
notification: { method: "cockpit/review_event", params: event },
|
|
4182
|
+
})
|
|
4183
|
+
return { status: "delivered", session: this._describe(session) }
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4139
4186
|
/**
|
|
4140
4187
|
* 指定 cwd の「権威セッション」を返す(T06407 増分2, axis2 の単一正本)。同一 cwd に複数
|
|
4141
4188
|
* ライブ codex セッションがあるとき、**最終活性が最新のもの**(=いまユーザーが使っている
|
|
@@ -4698,7 +4745,12 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4698
4745
|
producer_seq: session.seq,
|
|
4699
4746
|
thread_id: session.threadId,
|
|
4700
4747
|
code: 0,
|
|
4701
|
-
|
|
4748
|
+
// ⚠️ ここは「明示的に止めた理由」の許可リスト。載っていない理由はすべて "idle-reaped"
|
|
4749
|
+
// へ丸められ、利用者には「放置されて片付いた」と通知される。明示停止を足したら必ず
|
|
4750
|
+
// ここへも足すこと(足し忘れても停止自体は動くので、通知だけが静かに嘘になる)。
|
|
4751
|
+
reason: ["session-deleted", "chat-stop", "chat-stop-all", "worktree-remove"].includes(reason)
|
|
4752
|
+
? reason
|
|
4753
|
+
: "idle-reaped",
|
|
4702
4754
|
})
|
|
4703
4755
|
}
|
|
4704
4756
|
|
|
@@ -83,8 +83,14 @@ function globalMutationMode(type) {
|
|
|
83
83
|
return null
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// ⚠️ cwd を省略した attach は、ハンドラ側で config.claude_cwd に補われ、さらに claude では
|
|
87
|
+
// bridge が HOME へ補完して「その cwd の会話」になる (main.mjs の claude.attach / codex.attach
|
|
88
|
+
// と claude-stream-bridge の attach)。レーン分類がその補完を知らないと、同じ会話への attach が
|
|
89
|
+
// stream レーンへ、個別停止 (cwd を持つ) が cwd レーンへ散り、直列化されないまま同時に走る。
|
|
90
|
+
// scheduler へ設定も HOME も読ませず、補完後の既定 cwd を呼び出し側から mode 別に渡してもらう
|
|
91
|
+
// (補完の規則は CLI 種別で違うため、mode を知らない scheduler 側では決められない)。
|
|
92
|
+
function attachLaneKey(mode, msg, defaultCwd = "") {
|
|
93
|
+
const cwd = (typeof msg?.cwd === "string" ? msg.cwd.trim() : "") || defaultCwd
|
|
88
94
|
if (cwd) return `${mode}:cwd:${cwd}`
|
|
89
95
|
const resumeId =
|
|
90
96
|
mode === "claude"
|
|
@@ -100,14 +106,18 @@ function attachLaneKey(mode, msg) {
|
|
|
100
106
|
* run: (msg: object) => Promise<void>|void,
|
|
101
107
|
* logger?: {debug?: Function, warn?: Function, error?: Function},
|
|
102
108
|
* shouldBypass?: (type: string|undefined) => boolean,
|
|
109
|
+
* defaultCwd?: (mode: "claude"|"codex") => string,
|
|
103
110
|
* }} options
|
|
104
111
|
*/
|
|
105
112
|
export class DispatchScheduler {
|
|
106
|
-
constructor({ run, logger, shouldBypass }) {
|
|
113
|
+
constructor({ run, logger, shouldBypass, defaultCwd }) {
|
|
107
114
|
if (typeof run !== "function") throw new TypeError("DispatchScheduler run is required")
|
|
108
115
|
this.run = run
|
|
109
116
|
this.logger = logger
|
|
110
117
|
this.shouldBypass = typeof shouldBypass === "function" ? shouldBypass : () => false
|
|
118
|
+
// cwd 省略時にハンドラ/bridge が使う既定 cwd を CLI 種別ごとに返す関数。展開済みの値を
|
|
119
|
+
// 返すこと (scheduler は `~` 展開も HOME 参照もしない)。
|
|
120
|
+
this.defaultCwd = typeof defaultCwd === "function" ? defaultCwd : () => ""
|
|
111
121
|
this.generation = 0
|
|
112
122
|
this.sequence = 0
|
|
113
123
|
this.laneTails = new Map()
|
|
@@ -154,6 +164,21 @@ export class DispatchScheduler {
|
|
|
154
164
|
}
|
|
155
165
|
}
|
|
156
166
|
|
|
167
|
+
/**
|
|
168
|
+
* cwd 省略時にハンドラ/bridge が使う既定 cwd を、CLI 種別ごとに引く。
|
|
169
|
+
* ⚠️ レーン分類は全メッセージが通る経路なので、設定の読み取りが失敗しても dispatch を
|
|
170
|
+
* 止めない。取れなければ省略時の従来分類 (stream / control) へ戻す。
|
|
171
|
+
* @param {"claude"|"codex"} mode
|
|
172
|
+
*/
|
|
173
|
+
_resolveDefaultCwd(mode) {
|
|
174
|
+
try {
|
|
175
|
+
const cwd = this.defaultCwd(mode)
|
|
176
|
+
return typeof cwd === "string" ? cwd.trim() : ""
|
|
177
|
+
} catch {
|
|
178
|
+
return ""
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
157
182
|
_laneFor(msg) {
|
|
158
183
|
const type = msg?.type
|
|
159
184
|
const mutationMode = globalMutationMode(type)
|
|
@@ -163,20 +188,35 @@ export class DispatchScheduler {
|
|
|
163
188
|
// frontend の 10 秒打ち切りで捨てられていたため分離する (2026-09-04 本番計測)。
|
|
164
189
|
if (type === "usage.request") return "usage"
|
|
165
190
|
|
|
191
|
+
if (type === "chat.session.stop") {
|
|
192
|
+
if (msg.cli_kind === "claude") {
|
|
193
|
+
const cwd = typeof msg.cwd === "string" ? msg.cwd.trim() : ""
|
|
194
|
+
return cwd ? `claude:cwd:${cwd}` : "control"
|
|
195
|
+
}
|
|
196
|
+
if (msg.cli_kind === "codex" && msg.stream_id) {
|
|
197
|
+
return this.streamLaneKeys.get("codex:" + msg.stream_id) || `codex:stream:${msg.stream_id}`
|
|
198
|
+
}
|
|
199
|
+
return "control"
|
|
200
|
+
}
|
|
201
|
+
// chat.stop_all は全レーンに跨るため control に置く。その時点で live な会話だけを
|
|
202
|
+
// 止める契約であり、接続処理中のまだ live でない会話は対象外。sessions.request も control。
|
|
166
203
|
const mode = chatMode(type)
|
|
167
204
|
if (!mode) return "control"
|
|
168
205
|
|
|
169
206
|
const streamId = typeof msg?.stream_id === "string" ? msg.stream_id : ""
|
|
170
207
|
const streamKey = streamId ? `${mode}:${streamId}` : ""
|
|
208
|
+
const fallbackCwd = this._resolveDefaultCwd(mode)
|
|
171
209
|
if (type === `${mode}.attach`) {
|
|
172
|
-
const laneKey = attachLaneKey(mode, msg)
|
|
210
|
+
const laneKey = attachLaneKey(mode, msg, fallbackCwd)
|
|
173
211
|
this._rememberStreamLane(streamId, laneKey)
|
|
174
212
|
return laneKey
|
|
175
213
|
}
|
|
176
214
|
if (streamKey && this.streamLaneKeys.has(streamKey)) {
|
|
177
215
|
return this.streamLaneKeys.get(streamKey)
|
|
178
216
|
}
|
|
179
|
-
|
|
217
|
+
// attach と同じ補完を使う。片方だけ既定 cwd を見ると、同じ会話宛の upload / history 等が
|
|
218
|
+
// attach とは別レーンへ散り、attach で直したはずの取り違えが型ごと再発する。
|
|
219
|
+
const cwd = (typeof msg?.cwd === "string" ? msg.cwd.trim() : "") || fallbackCwd
|
|
180
220
|
if (cwd) return `${mode}:cwd:${cwd}`
|
|
181
221
|
if (streamId) return `${mode}:stream:${streamId}`
|
|
182
222
|
// codex.profile.list 等の stream 非依存 read は main 側の read-only bypass
|