@cocorograph/hub-agent 0.8.9 → 0.8.11
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 +24 -3
- package/src/claude-stream-bridge.mjs +68 -42
- package/src/codex-stream-bridge.mjs +223 -142
- package/src/main.mjs +93 -314
- package/src/sidebar-message-snapshot.mjs +362 -0
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -664,7 +664,13 @@ function takeTextsFromLine(line, user, assistant) {
|
|
|
664
664
|
return { user, assistant }
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
-
|
|
667
|
+
/**
|
|
668
|
+
* @param {string} filePath 対象 jsonl
|
|
669
|
+
* @param {{ isInjected?: (text: string) => Promise<boolean> | boolean }} [opts]
|
|
670
|
+
* 注入と判定した user ターンは、そのターンの assistant も含めて飛ばす。
|
|
671
|
+
*/
|
|
672
|
+
export async function extractLastTurnTexts(filePath, opts = {}) {
|
|
673
|
+
const isInjected = typeof opts.isInjected === "function" ? opts.isInjected : null
|
|
668
674
|
let handle
|
|
669
675
|
try {
|
|
670
676
|
handle = await open(filePath, "r")
|
|
@@ -716,11 +722,26 @@ export async function extractLastTurnTexts(filePath) {
|
|
|
716
722
|
continue
|
|
717
723
|
}
|
|
718
724
|
const res = takeTextsFromLine(lineBuf.toString("utf-8"), user, assistant)
|
|
725
|
+
if (isInjected && res.user && !user) {
|
|
726
|
+
let injected = false
|
|
727
|
+
try {
|
|
728
|
+
injected = (await isInjected(res.user)) === true
|
|
729
|
+
} catch {
|
|
730
|
+
// 注入判定の障害で通常の依頼まで消さない。
|
|
731
|
+
}
|
|
732
|
+
if (injected) {
|
|
733
|
+
// 後方走査中の assistant はこの注入 user より後ろ、つまり同じ注入ターンの
|
|
734
|
+
// 応答候補。依頼だけでなく応答も捨てて、更に前の自分のターンを探す。
|
|
735
|
+
user = ""
|
|
736
|
+
assistant = ""
|
|
737
|
+
continue
|
|
738
|
+
}
|
|
739
|
+
}
|
|
719
740
|
user = res.user
|
|
720
741
|
assistant = res.assistant
|
|
721
|
-
if (user && assistant) break
|
|
742
|
+
if (isInjected ? Boolean(user) : user && assistant) break
|
|
722
743
|
}
|
|
723
|
-
if (user && assistant) break
|
|
744
|
+
if (isInjected ? Boolean(user) : user && assistant) break
|
|
724
745
|
if (skipped > LAST_TURN_MAX_SKIPPED_LINES) break
|
|
725
746
|
|
|
726
747
|
if (carry && carry.length > LAST_TURN_MAX_LINE) {
|
|
@@ -523,11 +523,11 @@ export class ClaudeStreamSession {
|
|
|
523
523
|
this._permissionResolvers = new Map()
|
|
524
524
|
/** 現在ターン実行中か (多重 query 防止) */
|
|
525
525
|
this._busy = false
|
|
526
|
-
/** SDK-P0-3 (2026-07-10): 現ターンが既に終端処理済みか (
|
|
526
|
+
/** SDK-P0-3 (2026-07-10): 現ターンが既に終端処理済みか (idle / abort の
|
|
527
527
|
* いずれか 1 回だけ _busy=false + drain させるための冪等ガード)。resident query は
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
528
|
+
* `result` の後にもバックグラウンド処理が続き、最後に権威的な
|
|
529
|
+
* `session_state_changed:idle` が流れる。通常完了では idle だけが settle し、
|
|
530
|
+
* 明示 interrupt だけが先行して settled を立てる。ターン開始時に false へ戻す。 */
|
|
531
531
|
this._turnSettled = false
|
|
532
532
|
/** SDK-P0-3: 現ターンが 1 件でもストリームイベントを出したか。
|
|
533
533
|
* 「前ターンの遅延 idle が、drain で始まったばかりの次ターンを誤終了させる」レースの
|
|
@@ -539,9 +539,9 @@ export class ClaudeStreamSession {
|
|
|
539
539
|
this._turnProgressed = false
|
|
540
540
|
/** SDK-P0-4 (2026-07-13): ターン連番。ログと帰属デバッグ用 (_beginResidentTurn で ++)。 */
|
|
541
541
|
this._turnSeq = 0
|
|
542
|
-
/** SDK-P0-4: 「直前ターンの idle がこれから 1 件届きうる」状態。
|
|
543
|
-
*
|
|
544
|
-
*
|
|
542
|
+
/** SDK-P0-4: 「直前ターンの idle がこれから 1 件届きうる」状態。abort (interrupt) で
|
|
543
|
+
* ターンを先行して畳んだときに arm する (interrupt 後にも中断ターンの idle が流れるため)。
|
|
544
|
+
* 通常完了は idle 自身で畳むので arm しない。arm 中に届いた idle は
|
|
545
545
|
* **前ターンのもの**として 1 件だけ握り潰し、現ターンを終端しない。
|
|
546
546
|
* 解除条件: (a) idle を 1 件消費した (b) SDK の running が届いた (= 前ターンの全イベントは
|
|
547
547
|
* 順序保証により既に通過済み) (c) 現ターンの result が届いた (d) STALE_IDLE_GUARD_MS 経過。 */
|
|
@@ -1131,9 +1131,10 @@ export class ClaudeStreamSession {
|
|
|
1131
1131
|
this._busy = true
|
|
1132
1132
|
this._abortController = new AbortController()
|
|
1133
1133
|
let aborted = false
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1134
|
+
// `session_state_changed:idle` がターン終了の正本。SDK が idle を流さず generator が
|
|
1135
|
+
// 閉じた旧/異常経路だけ合成 idle と onTurnSettled で waiting へ落とす。
|
|
1136
1136
|
let resultDelivered = false
|
|
1137
|
+
let idleDelivered = false
|
|
1137
1138
|
|
|
1138
1139
|
const options = {
|
|
1139
1140
|
cwd: this.cwd,
|
|
@@ -1185,6 +1186,7 @@ export class ClaudeStreamSession {
|
|
|
1185
1186
|
resultDelivered = true
|
|
1186
1187
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|
|
1187
1188
|
}
|
|
1189
|
+
if (isSessionIdleEvent(msg)) idleDelivered = true
|
|
1188
1190
|
try {
|
|
1189
1191
|
this._emit(msg)
|
|
1190
1192
|
} catch (err) {
|
|
@@ -1224,12 +1226,9 @@ export class ClaudeStreamSession {
|
|
|
1224
1226
|
if (aborted) {
|
|
1225
1227
|
this.logger?.info({ stream_id: this.stream_id }, "claude turn aborted")
|
|
1226
1228
|
}
|
|
1227
|
-
// RC-4:
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
// result が既に waiting にしているので !resultDelivered のときだけ。_drainPending で
|
|
1231
|
-
// 次ターン (processing) が始まる前に呼ぶこと (順序: ここ → reap/drain)。
|
|
1232
|
-
if (!resultDelivered) {
|
|
1229
|
+
// RC-4: idle を届けずに generator が閉じた旧/異常経路では、チャット信号と browser の
|
|
1230
|
+
// 双方へ同じ終了境界を補う。_drainPending で次ターンを始める前に行うこと。
|
|
1231
|
+
if (!idleDelivered) {
|
|
1233
1232
|
try {
|
|
1234
1233
|
this.onTurnSettled?.()
|
|
1235
1234
|
} catch {
|
|
@@ -1244,11 +1243,24 @@ export class ClaudeStreamSession {
|
|
|
1244
1243
|
// フッターで表示する)。uuid を持たせるのは重複排除 (isDuplicateEvent) が result
|
|
1245
1244
|
// 署名 (session_id + num_turns + duration_ms + cost) で判定するため — 合成 result
|
|
1246
1245
|
// はこれらが毎回同値になり、2 回目以降の中断で「重複」と誤判定され捨てられるのを防ぐ。
|
|
1246
|
+
if (!resultDelivered) {
|
|
1247
|
+
try {
|
|
1248
|
+
this._emit({
|
|
1249
|
+
type: "result",
|
|
1250
|
+
subtype: aborted ? "aborted_by_user" : "turn_settled",
|
|
1251
|
+
uuid: randomUUID(),
|
|
1252
|
+
session_id: this.sessionId ?? undefined,
|
|
1253
|
+
timestamp: new Date().toISOString(),
|
|
1254
|
+
})
|
|
1255
|
+
} catch {
|
|
1256
|
+
/* ignore */
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1247
1259
|
try {
|
|
1248
1260
|
this._emit({
|
|
1249
|
-
type: "
|
|
1250
|
-
subtype:
|
|
1251
|
-
|
|
1261
|
+
type: "system",
|
|
1262
|
+
subtype: "session_state_changed",
|
|
1263
|
+
state: "idle",
|
|
1252
1264
|
session_id: this.sessionId ?? undefined,
|
|
1253
1265
|
timestamp: new Date().toISOString(),
|
|
1254
1266
|
})
|
|
@@ -1618,10 +1630,10 @@ export class ClaudeStreamSession {
|
|
|
1618
1630
|
}
|
|
1619
1631
|
}
|
|
1620
1632
|
|
|
1621
|
-
/**
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
* @param {string} reason "
|
|
1633
|
+
/** resident の権威的な「ターン終端」を 1 箇所に集約する。
|
|
1634
|
+
* 通常完了は SDK の session_state_changed:idle だけが呼ぶ。result は最終結果の
|
|
1635
|
+
* flush であり、バックグラウンド処理を含むターン全体の終了境界ではない。
|
|
1636
|
+
* @param {string} reason "idle" — ログ用
|
|
1625
1637
|
* @param {(msg: string) => void} [denyPending] 未解決 permission を閉じるクロージャ
|
|
1626
1638
|
*/
|
|
1627
1639
|
_settleResidentTurn(reason, denyPending) {
|
|
@@ -1629,11 +1641,6 @@ export class ClaudeStreamSession {
|
|
|
1629
1641
|
this._turnSettled = true
|
|
1630
1642
|
denyPending?.("turn ended")
|
|
1631
1643
|
this._busy = false
|
|
1632
|
-
// SDK-P0-4: result で畳んだターンは、この後に自分の idle を必ず 1 件流す
|
|
1633
|
-
// (sdk.d.ts: "'idle' fires after heldBackResult flushes … authoritative turn-over signal")。
|
|
1634
|
-
// drain が同期で次ターンを開始するため、その idle が新ターンに誤帰属しないようガードを張る。
|
|
1635
|
-
// idle 自身で畳んだ場合は既にその idle を消費済みなので張らない。
|
|
1636
|
-
if (reason === "result") this._armStaleIdleGuard()
|
|
1637
1644
|
// 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
|
|
1638
1645
|
// ultracode (0.6.28): _drainResidentPending は applyFlagSettings を await するため
|
|
1639
1646
|
// async。for await ループ内からは fire-and-forget で呼ぶ (最初の await より前に
|
|
@@ -1649,7 +1656,7 @@ export class ClaudeStreamSession {
|
|
|
1649
1656
|
/** 改修4 (A): ターン完了時に pending の先頭 1 件を InputQueue へ流す (ターンのシリアライズ)。
|
|
1650
1657
|
* queue_state を更新して送信待ちチップを drain させる (frontend がバブル昇格する)。
|
|
1651
1658
|
* ultracode (0.6.28): 次ターンの目標 ultracode 状態へ reconcile してから push する
|
|
1652
|
-
* (await するため async 化。
|
|
1659
|
+
* (await するため async 化。idle ハンドラからは fire-and-forget で呼ばれる)。 */
|
|
1653
1660
|
async _drainResidentPending() {
|
|
1654
1661
|
if (this._closed) return
|
|
1655
1662
|
if (this._pendingMessages.length === 0) {
|
|
@@ -1693,7 +1700,7 @@ export class ClaudeStreamSession {
|
|
|
1693
1700
|
|
|
1694
1701
|
/** 改修2+4: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
|
|
1695
1702
|
* system/init は最初の 1 回のみ (毎ターン init = 文脈/キャッシュ温め直しを解消)。各ターンは
|
|
1696
|
-
*
|
|
1703
|
+
* 権威的終端シグナル (session_state_changed:idle) で _busy=false にするが query は継続し
|
|
1697
1704
|
* 次 input を待つ。close (inputQueue.close) で generator が終わり query が完走する。
|
|
1698
1705
|
* SDK-P0-3 (2026-07-10): 終端を result だけに頼ると、SDK が idle でターンを終えたときに
|
|
1699
1706
|
* _busy が固着し、以降の送信が pending へ永久滞留する (T06052)。
|
|
@@ -1749,6 +1756,7 @@ export class ClaudeStreamSession {
|
|
|
1749
1756
|
const q = this.sdk.query({ prompt: this._inputQueue, options })
|
|
1750
1757
|
this._residentQuery = q
|
|
1751
1758
|
for await (const msg of q) {
|
|
1759
|
+
let suppressEmit = false
|
|
1752
1760
|
if (
|
|
1753
1761
|
msg?.type === "system" &&
|
|
1754
1762
|
msg?.subtype === "init" &&
|
|
@@ -1758,12 +1766,12 @@ export class ClaudeStreamSession {
|
|
|
1758
1766
|
}
|
|
1759
1767
|
if (msg?.type === "result") {
|
|
1760
1768
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1769
|
+
// result は結果 flush でありターン終了ではない。SDK の型定義が権威的な
|
|
1770
|
+
// turn-over signal とする session_state_changed:idle まで busy を維持し、
|
|
1771
|
+
// pending を streaming input へ流さない。
|
|
1772
|
+
// 現ターンの result が届いた時点で前ターンの遅延 idle は順序保証により
|
|
1773
|
+
// 既に通過済みなので、interrupt 由来のガードだけ解除できる。
|
|
1764
1774
|
this._clearStaleIdleGuard()
|
|
1765
|
-
// ターン完了: 未解決 permission を閉じ、次 input 受付へ (query は継続)。
|
|
1766
|
-
this._settleResidentTurn("result", denyPending)
|
|
1767
1775
|
} else if (isSessionIdleEvent(msg)) {
|
|
1768
1776
|
// SDK-P0-3 (2026-07-10): result が届かずに idle でターンが終わるケースの救済。
|
|
1769
1777
|
// これを見ていなかったため _busy が true で固着し、以降の送信が
|
|
@@ -1777,11 +1785,14 @@ export class ClaudeStreamSession {
|
|
|
1777
1785
|
// 2) それ以外で _busy かつ (running を観測済み or 何かイベントを出した) → 現ターンの終端。
|
|
1778
1786
|
if (this._staleIdleExpected) {
|
|
1779
1787
|
this._clearStaleIdleGuard()
|
|
1788
|
+
// browser にも転送しない。前ターンの遅延 idle を新ターンの reducer が受けると、
|
|
1789
|
+
// 3点リーダーと送信ボタンだけが誤って idle になる。
|
|
1790
|
+
suppressEmit = true
|
|
1780
1791
|
this.logger?.debug?.(
|
|
1781
1792
|
{ stream_id: this.stream_id, turn: this._turnSeq },
|
|
1782
1793
|
"stale idle from previous turn ignored",
|
|
1783
1794
|
)
|
|
1784
|
-
} else if (this._busy
|
|
1795
|
+
} else if (this._busy) {
|
|
1785
1796
|
this._settleResidentTurn("idle", denyPending)
|
|
1786
1797
|
// per-message の RC-4 と対称に、チャット信号 (サイドバードット) を
|
|
1787
1798
|
// processing→waiting へ落とす。result 経由なら main.mjs が result を見て
|
|
@@ -1804,13 +1815,15 @@ export class ClaudeStreamSession {
|
|
|
1804
1815
|
// および session_state_changed の running / requires_action もここに含む。
|
|
1805
1816
|
this._turnProgressed = true
|
|
1806
1817
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1818
|
+
if (!suppressEmit) {
|
|
1819
|
+
try {
|
|
1820
|
+
this._emit(msg)
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
this.logger?.warn(
|
|
1823
|
+
{ err: err.message, stream_id: this.stream_id },
|
|
1824
|
+
"onEvent callback threw",
|
|
1825
|
+
)
|
|
1826
|
+
}
|
|
1814
1827
|
}
|
|
1815
1828
|
}
|
|
1816
1829
|
} catch (err) {
|
|
@@ -1949,6 +1962,19 @@ export class ClaudeStreamSession {
|
|
|
1949
1962
|
} catch {
|
|
1950
1963
|
/* ignore */
|
|
1951
1964
|
}
|
|
1965
|
+
// 通常完了と同じ権威境界を browser へ即時提示する。SDK が後から流す
|
|
1966
|
+
// 中断ターン由来の実 idle は _staleIdleExpected で抑止する。
|
|
1967
|
+
try {
|
|
1968
|
+
this._emit({
|
|
1969
|
+
type: "system",
|
|
1970
|
+
subtype: "session_state_changed",
|
|
1971
|
+
state: "idle",
|
|
1972
|
+
session_id: this.sessionId ?? undefined,
|
|
1973
|
+
timestamp: new Date().toISOString(),
|
|
1974
|
+
})
|
|
1975
|
+
} catch {
|
|
1976
|
+
/* ignore */
|
|
1977
|
+
}
|
|
1952
1978
|
}
|
|
1953
1979
|
// T05699 残タスク #2-1 (2026-07-04): interrupt() は現ターンの result を発火しない
|
|
1954
1980
|
// (query は継続、次 input 受付継続のみ)。そのため abort 時点で _pendingMessages に
|