@cocorograph/hub-agent 0.8.62 → 0.8.64
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 +43 -1
- package/src/claude-stream-bridge.mjs +136 -2
- package/src/codex-stream-bridge.mjs +19 -0
- package/src/main.mjs +37 -0
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { randomUUID } from "node:crypto"
|
|
19
19
|
import { constants as fsConstants } from "node:fs"
|
|
20
|
-
import { open, readFile, readdir, stat } from "node:fs/promises"
|
|
20
|
+
import { open, readFile, readdir, rm, stat, unlink } from "node:fs/promises"
|
|
21
21
|
import os from "node:os"
|
|
22
22
|
import path from "node:path"
|
|
23
23
|
|
|
@@ -646,6 +646,48 @@ export async function renameSession({ cwd, session_id, title, projectsRoot, logg
|
|
|
646
646
|
}
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
+
/** Claude セッションの JSONL と同名の補助ディレクトリを削除する。 */
|
|
650
|
+
export async function deleteSession({
|
|
651
|
+
cwd,
|
|
652
|
+
session_id,
|
|
653
|
+
projectsRoot,
|
|
654
|
+
logger,
|
|
655
|
+
rmImpl = rm,
|
|
656
|
+
unlinkImpl = unlink,
|
|
657
|
+
statImpl = stat,
|
|
658
|
+
}) {
|
|
659
|
+
if (typeof cwd !== "string" || cwd.length === 0) return { ok: false, error: "cwd required" }
|
|
660
|
+
if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(session_id)) {
|
|
661
|
+
return { ok: false, error: "invalid session_id" }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const filePath = jsonlPath({ cwd, session_id, projectsRoot })
|
|
665
|
+
const dirPath = filePath.slice(0, -".jsonl".length)
|
|
666
|
+
try {
|
|
667
|
+
const fileStat = await statImpl(filePath)
|
|
668
|
+
if (!fileStat.isFile()) return { ok: false, error: "session not found" }
|
|
669
|
+
} catch (err) {
|
|
670
|
+
const notFound = err?.code === "ENOENT" || err?.code === "EISDIR"
|
|
671
|
+
if (notFound) return { ok: false, error: "session not found" }
|
|
672
|
+
logger?.warn({ err: err?.message || String(err), code: err?.code, filePath }, "claude session delete stat failed")
|
|
673
|
+
return { ok: false, error: "delete failed" }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
await rmImpl(dirPath, { recursive: true, force: true })
|
|
678
|
+
await unlinkImpl(filePath)
|
|
679
|
+
} catch (err) {
|
|
680
|
+
logger?.warn({ err: err?.message || String(err), code: err?.code, filePath }, "claude session delete failed")
|
|
681
|
+
return { ok: false, error: "delete failed" }
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
hydrateCache.delete(filePath)
|
|
685
|
+
const parentDir = path.dirname(filePath)
|
|
686
|
+
const fileMap = lastTurnDirCache.get(parentDir)
|
|
687
|
+
fileMap?.delete(filePath)
|
|
688
|
+
return { ok: true }
|
|
689
|
+
}
|
|
690
|
+
|
|
649
691
|
/**
|
|
650
692
|
* jsonl 1 ファイルを読んで UI 表示用イベント配列を返す。
|
|
651
693
|
*
|
|
@@ -538,6 +538,15 @@ export class ClaudeStreamSession {
|
|
|
538
538
|
* InputQueue にメッセージが消えることがある。このフラグで「push 直前に本当にまだ
|
|
539
539
|
* 生きているか」を再確認する。新しい query を (再)起動するたびに false へリセットする。 */
|
|
540
540
|
this._residentQueryReturned = false
|
|
541
|
+
/** 削除時に CLI 終了を待つための、活動中 run-loop の完了 Promise 集合。
|
|
542
|
+
* ⚠️ 単一の「現在の Promise」にしないこと (cross-review blocker, 2026-09-05): per-message の
|
|
543
|
+
* finally は `_drainPending()` で次ターンを**同期的に**起動してから自分の完了処理へ進むため、
|
|
544
|
+
* 単一スロットだと旧ターンの完了が新ターンの Promise を誤って resolve し、CLI が走行中でも
|
|
545
|
+
* waitForRunLoopExit() が即 resolve して jsonl を unlink できてしまう (resident の異常再起動も同型)。
|
|
546
|
+
* run-loop ごとに自分の Promise を持ち、自分のものだけを完了させる。 */
|
|
547
|
+
this._activeRunLoops = new Set()
|
|
548
|
+
/** 削除対象では close 後の tmp→rename が JSONL を復活させないよう at-rest mask を止める。 */
|
|
549
|
+
this._suppressAtRestMask = false
|
|
541
550
|
|
|
542
551
|
/** 改修3: per-message セッションで busy 中に届いた送信を退避する pending キュー。
|
|
543
552
|
* 常駐 query 化 (改修2) とは別レイヤー。resume チェーンは維持したまま、現ターン
|
|
@@ -554,6 +563,9 @@ export class ClaudeStreamSession {
|
|
|
554
563
|
* メッセージが後着して実行される「ゴースト実行」を防ぐ)。到着で消費 (delete)。
|
|
555
564
|
* 無限増加を避けるため上限で全クリアする (キャンセルは低頻度)。 */
|
|
556
565
|
this._canceledInputIds = new Set()
|
|
566
|
+
/** 取消成功した input_id の直近リング。accepted は配達確認と二重実行防止の根拠なので外さず、
|
|
567
|
+
* browser が「accepted + キュー不在」のバブル昇格から取消済み id を除外する別軸にする。 */
|
|
568
|
+
this._cancelledInputIds = []
|
|
557
569
|
/** 改修3: 直近 browser へ通知した queue 署名 (件数 + id 列)。変化時のみ emit する。
|
|
558
570
|
* 空キューの署名 ("0:") で初期化し、空→空の冗長 emit を抑止する。 */
|
|
559
571
|
this._lastEmittedQueueSig = "0:"
|
|
@@ -849,6 +861,7 @@ export class ClaudeStreamSession {
|
|
|
849
861
|
* ファイルを掴んでいない「終了後」にのみ呼ぶこと (ライブ追記中の書き換えは会話本体を壊す)。
|
|
850
862
|
* display は _emit で常時マスク済みのため、これは at-rest ファイルの掃除専用。 */
|
|
851
863
|
_maskJsonlAtRest() {
|
|
864
|
+
if (this._suppressAtRestMask) return
|
|
852
865
|
if (this._jsonlMasked || !MASK_ENABLED) return
|
|
853
866
|
if (!this.sessionId || !this.cwd) return
|
|
854
867
|
this._jsonlMasked = true
|
|
@@ -1303,7 +1316,10 @@ export class ClaudeStreamSession {
|
|
|
1303
1316
|
// チップを固着させる。ack を返す条件 (accepted) と同じ場所で 1 回だけ記録する。
|
|
1304
1317
|
const inputId =
|
|
1305
1318
|
typeof opts?.input_id === "string" && opts.input_id ? opts.input_id : null
|
|
1306
|
-
if (inputId && result?.accepted)
|
|
1319
|
+
if (inputId && result?.accepted) {
|
|
1320
|
+
this._recordAcceptedInput(inputId)
|
|
1321
|
+
if (result.preCancelled) this._recordCancelledInput(inputId)
|
|
1322
|
+
}
|
|
1307
1323
|
return result
|
|
1308
1324
|
}
|
|
1309
1325
|
|
|
@@ -1326,7 +1342,7 @@ export class ClaudeStreamSession {
|
|
|
1326
1342
|
{ stream_id: this.stream_id, input_id: inputId },
|
|
1327
1343
|
"dropped pre-canceled input (cancel arrived before input)",
|
|
1328
1344
|
)
|
|
1329
|
-
return { accepted: true }
|
|
1345
|
+
return { accepted: true, preCancelled: true }
|
|
1330
1346
|
}
|
|
1331
1347
|
|
|
1332
1348
|
// ultracode ワンショット (0.6.28): このターンのみ xhigh effort + 常時 dynamic-workflow
|
|
@@ -1421,6 +1437,7 @@ export class ClaudeStreamSession {
|
|
|
1421
1437
|
/** per-message 1 ターンを実行する (resume チェーン)。busy 中に届いた送信は sendMessage
|
|
1422
1438
|
* が pending キューへ退避し、本メソッドの finally で drain する。 */
|
|
1423
1439
|
async _runPerMessage(prompt, opts = {}) {
|
|
1440
|
+
const finishRunLoop = this._markRunLoopStarted()
|
|
1424
1441
|
this._busy = true
|
|
1425
1442
|
this._abortController = new AbortController()
|
|
1426
1443
|
let aborted = false
|
|
@@ -1612,6 +1629,7 @@ export class ClaudeStreamSession {
|
|
|
1612
1629
|
// jsonl を掴んでいない) のこの地点で、closed 済みならオンディスク jsonl をマスクする。
|
|
1613
1630
|
// reap 経路は上の close() 内で既にマスク済み (冪等ガードで二重実行されない)。
|
|
1614
1631
|
if (this._closed) this._maskJsonlAtRest()
|
|
1632
|
+
finishRunLoop()
|
|
1615
1633
|
}
|
|
1616
1634
|
}
|
|
1617
1635
|
|
|
@@ -1701,6 +1719,8 @@ export class ClaudeStreamSession {
|
|
|
1701
1719
|
* snapshot の `accepted` として browser へ載せ、ack 喪失時の自己回復に使う。 */
|
|
1702
1720
|
_recordAcceptedInput(inputId) {
|
|
1703
1721
|
if (typeof inputId !== "string" || !inputId) return
|
|
1722
|
+
const cancelledAt = this._cancelledInputIds.indexOf(inputId)
|
|
1723
|
+
if (cancelledAt !== -1) this._cancelledInputIds.splice(cancelledAt, 1)
|
|
1704
1724
|
const at = this._acceptedInputIds.indexOf(inputId)
|
|
1705
1725
|
if (at !== -1) this._acceptedInputIds.splice(at, 1)
|
|
1706
1726
|
this._acceptedInputIds.push(inputId)
|
|
@@ -1712,6 +1732,19 @@ export class ClaudeStreamSession {
|
|
|
1712
1732
|
}
|
|
1713
1733
|
}
|
|
1714
1734
|
|
|
1735
|
+
_recordCancelledInput(inputId) {
|
|
1736
|
+
if (typeof inputId !== "string" || !inputId) return
|
|
1737
|
+
const at = this._cancelledInputIds.indexOf(inputId)
|
|
1738
|
+
if (at !== -1) this._cancelledInputIds.splice(at, 1)
|
|
1739
|
+
this._cancelledInputIds.push(inputId)
|
|
1740
|
+
if (this._cancelledInputIds.length > ACCEPTED_INPUT_ID_RING_MAX) {
|
|
1741
|
+
this._cancelledInputIds.splice(
|
|
1742
|
+
0,
|
|
1743
|
+
this._cancelledInputIds.length - ACCEPTED_INPUT_ID_RING_MAX,
|
|
1744
|
+
)
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1715
1748
|
/** キャンセル機能 (0.6.26): browser から id 指定で送信待ちメッセージを 1 件取り消す。
|
|
1716
1749
|
* 実行開始済み (既に drain された) メッセージは pending に残っていないので no-op になる。
|
|
1717
1750
|
* 削除に成功したら queue_state を再 emit して送信待ちチップを更新する。 */
|
|
@@ -1728,6 +1761,8 @@ export class ClaudeStreamSession {
|
|
|
1728
1761
|
this._pendingMessages = this._pendingMessages.filter((m) => m.id !== id)
|
|
1729
1762
|
const removed = before !== this._pendingMessages.length
|
|
1730
1763
|
if (removed) {
|
|
1764
|
+
this._canceledInputIds.delete(id)
|
|
1765
|
+
this._recordCancelledInput(id)
|
|
1731
1766
|
this.logger?.info(
|
|
1732
1767
|
{ stream_id: this.stream_id, id, queued: this._pendingMessages.length },
|
|
1733
1768
|
"queued message canceled",
|
|
@@ -1843,6 +1878,7 @@ export class ClaudeStreamSession {
|
|
|
1843
1878
|
// と判定して楽観チップを消し、「キューに無い + ここにも無い = 未配達」なら
|
|
1844
1879
|
// 消さずに再送導線を出す (無言削除の回帰を作らないための 2 軸判定)。
|
|
1845
1880
|
accepted: [...this._acceptedInputIds],
|
|
1881
|
+
cancelled: [...this._cancelledInputIds],
|
|
1846
1882
|
}
|
|
1847
1883
|
}
|
|
1848
1884
|
|
|
@@ -2083,6 +2119,7 @@ export class ClaudeStreamSession {
|
|
|
2083
2119
|
* 常駐では query stream が唯一のイベント源のため watcher は張らない (過去確定分は browser が
|
|
2084
2120
|
* history.request で hydrate 済み。resume で query stream が過去行を replay しない前提)。 */
|
|
2085
2121
|
async _runResidentQuery() {
|
|
2122
|
+
const finishRunLoop = this._markRunLoopStarted()
|
|
2086
2123
|
// SDK-P0-4 (2026-07-13): 常駐にも AbortController を渡す (per-message には元からある)。
|
|
2087
2124
|
// 正規の中断は interrupt() だが、control request が失敗/無応答のときに generator を畳む
|
|
2088
2125
|
// 最終手段が無いと、close()/shutdown() で CLI 子プロセスが残留する。
|
|
@@ -2298,6 +2335,7 @@ export class ClaudeStreamSession {
|
|
|
2298
2335
|
// マスク (2026-06-23): 常駐 query 終了後 (= CLI が jsonl を掴んでいない) のこの地点で、
|
|
2299
2336
|
// closed 済みならオンディスク jsonl をマスクする。reap 経路は close() 内で実行済み。
|
|
2300
2337
|
if (this._closed) this._maskJsonlAtRest()
|
|
2338
|
+
finishRunLoop()
|
|
2301
2339
|
}
|
|
2302
2340
|
}
|
|
2303
2341
|
|
|
@@ -2473,6 +2511,34 @@ export class ClaudeStreamSession {
|
|
|
2473
2511
|
return true
|
|
2474
2512
|
}
|
|
2475
2513
|
|
|
2514
|
+
/** run-loop 開始を登録し、**その run-loop 自身だけ**を完了させる関数を返す (冪等)。 */
|
|
2515
|
+
_markRunLoopStarted() {
|
|
2516
|
+
let resolve
|
|
2517
|
+
const promise = new Promise((r) => {
|
|
2518
|
+
resolve = r
|
|
2519
|
+
})
|
|
2520
|
+
this._activeRunLoops.add(promise)
|
|
2521
|
+
return () => {
|
|
2522
|
+
if (!this._activeRunLoops.delete(promise)) return
|
|
2523
|
+
resolve()
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
/** 削除専用 close。通常 close の at-rest mask だけを抑止する。 */
|
|
2528
|
+
closeForDeletion() {
|
|
2529
|
+
this._suppressAtRestMask = true
|
|
2530
|
+
this.close()
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/** 呼び出し時点で活動中の run-loop が**すべて**終わるまで待つ。待っている間に新しい run-loop が
|
|
2534
|
+
* 同期起動された (pending drain / resident 再起動) 場合はそれも待つ。close 済みなら
|
|
2535
|
+
* `_drainPending()` / 再起動は `_closed` ガードで始まらないため、連鎖は有限で終わる。 */
|
|
2536
|
+
async waitForRunLoopExit() {
|
|
2537
|
+
while (this._activeRunLoops.size > 0) {
|
|
2538
|
+
await Promise.all([...this._activeRunLoops])
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2476
2542
|
/** セッション終了。新規ターンを止め、実行中なら中断し、watcher も停止する。 */
|
|
2477
2543
|
close() {
|
|
2478
2544
|
this._closed = true
|
|
@@ -2554,6 +2620,12 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2554
2620
|
/** session_id → 生存セッション。再接続 (再アタッチ) で同一セッションを引く索引。
|
|
2555
2621
|
* browser が切れてもここに残し、走行中ターンの継続 + 再接続でのライブ追従を可能にする。 */
|
|
2556
2622
|
this._liveBySession = new Map()
|
|
2623
|
+
/** key(cwd+session_id) → 削除停止を開始済みで run-loop 終了待ちのセッション集合。
|
|
2624
|
+
* 索引から外した後も再試行が同じ CLI 群を待てるように保持し、各 CLI の終了時に外す。
|
|
2625
|
+
* 同じ key で新しい CLI が起動→再削除→timeout した場合も全件残す (1 件だけだと 2 件目が追跡漏れ)。 */
|
|
2626
|
+
this._deletionTombstones = new Map()
|
|
2627
|
+
/** テストでは短縮可能。実運用では CLI reap を最大 5 秒待つ。 */
|
|
2628
|
+
this._sessionDeletionWaitMs = 5_000
|
|
2557
2629
|
|
|
2558
2630
|
/** MCP control 専用 query (agent-global)。遅延起動・アイドル破棄。session 非依存。 */
|
|
2559
2631
|
this._mcpControlQuery = null
|
|
@@ -2660,6 +2732,68 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2660
2732
|
return stopped
|
|
2661
2733
|
}
|
|
2662
2734
|
|
|
2735
|
+
/** cwd と session_id が完全一致する live セッションだけを停止し、CLI 終了まで待つ。
|
|
2736
|
+
* 戻り値は今回新たに停止した数 (tombstone の待ち直しは数えない)。timeout は throw。 */
|
|
2737
|
+
async stopSessionForDeletion({ cwd, session_id }) {
|
|
2738
|
+
const key = this._liveKeyFor(cwd, session_id)
|
|
2739
|
+
const newVictims = new Set()
|
|
2740
|
+
for (const s of [...this._liveBySession.values(), ...this.sessions.values()]) {
|
|
2741
|
+
if (s.cwd === cwd && s.sessionId === session_id && !s._closed) newVictims.add(s)
|
|
2742
|
+
}
|
|
2743
|
+
const waitTargets = new Set(newVictims)
|
|
2744
|
+
for (const s of this._deletionTombstones.get(key) ?? []) waitTargets.add(s)
|
|
2745
|
+
|
|
2746
|
+
for (const s of newVictims) {
|
|
2747
|
+
this._dropSessionMappings(s)
|
|
2748
|
+
s.closeForDeletion()
|
|
2749
|
+
this._rememberDeletionTombstone(key, s)
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
let timer
|
|
2753
|
+
try {
|
|
2754
|
+
// 全対象を 1 本の Promise にまとめて待つ (逐次だと先頭の timeout で後続を確認しないまま抜ける)。
|
|
2755
|
+
await Promise.race([
|
|
2756
|
+
Promise.all([...waitTargets].map((s) => s.waitForRunLoopExit())),
|
|
2757
|
+
new Promise((_, reject) => {
|
|
2758
|
+
timer = setTimeout(() => reject(new Error("session deletion stop timeout")), this._sessionDeletionWaitMs)
|
|
2759
|
+
timer.unref?.()
|
|
2760
|
+
}),
|
|
2761
|
+
])
|
|
2762
|
+
} finally {
|
|
2763
|
+
if (timer) clearTimeout(timer)
|
|
2764
|
+
// timeout で throw する経路でも exit は必ず出す: session は既に close 済みで索引からも
|
|
2765
|
+
// 外れているため、ここで送らないと browser は「生きている stream」を抱えたままになる
|
|
2766
|
+
// (削除自体は main が `delete failed` で止める。exit と失敗応答は独立)。
|
|
2767
|
+
// tombstone からの待ち直しは初回に exit 済みなので出さない (1 セッション 1 回)。
|
|
2768
|
+
for (const s of newVictims) {
|
|
2769
|
+
this.emit("exit", {
|
|
2770
|
+
stream_id: s.stream_id,
|
|
2771
|
+
code: 0,
|
|
2772
|
+
reason: "session-deleted",
|
|
2773
|
+
session_id: s.sessionId,
|
|
2774
|
+
})
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
return newVictims.size
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/** 削除停止したセッションを key ごとの集合に残し、その run-loop が終わったときだけ外す。 */
|
|
2781
|
+
_rememberDeletionTombstone(key, session) {
|
|
2782
|
+
if (!key) return
|
|
2783
|
+
let set = this._deletionTombstones.get(key)
|
|
2784
|
+
if (!set) {
|
|
2785
|
+
set = new Set()
|
|
2786
|
+
this._deletionTombstones.set(key, set)
|
|
2787
|
+
}
|
|
2788
|
+
set.add(session)
|
|
2789
|
+
session.waitForRunLoopExit().then(() => {
|
|
2790
|
+
const cur = this._deletionTombstones.get(key)
|
|
2791
|
+
if (!cur) return
|
|
2792
|
+
cur.delete(session)
|
|
2793
|
+
if (cur.size === 0) this._deletionTombstones.delete(key)
|
|
2794
|
+
})
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2663
2797
|
/**
|
|
2664
2798
|
* 新しい Claude セッションを開始する。
|
|
2665
2799
|
*
|
|
@@ -973,6 +973,9 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
973
973
|
queueStateSeq: 0,
|
|
974
974
|
/** ack 喪失からの自己回復用、受理済み browser input_id の bounded ring。 */
|
|
975
975
|
acceptedInputIds: [],
|
|
976
|
+
/** 取消成功 id は accepted(二重実行ガード)から外さず、browser が「accepted +キュー不在」
|
|
977
|
+
* のバブル昇格から除外するための別軸として保持する。 */
|
|
978
|
+
cancelledInputIds: [],
|
|
976
979
|
/** @type {{resolve: Function, timer: any} | null} round3 major#2: turn/start 後に turn/started
|
|
977
980
|
* (activeTurnId 確定) を待つ barrier の待機者。timeout 付きでハングを防ぐ。 */
|
|
978
981
|
_turnActiveWaiter: null,
|
|
@@ -2062,6 +2065,9 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2062
2065
|
|
|
2063
2066
|
_recordAcceptedInput(session, inputId) {
|
|
2064
2067
|
if (!session || typeof inputId !== "string" || !inputId) return
|
|
2068
|
+
const cancelledIds = session.cancelledInputIds || (session.cancelledInputIds = [])
|
|
2069
|
+
const cancelledAt = cancelledIds.indexOf(inputId)
|
|
2070
|
+
if (cancelledAt !== -1) cancelledIds.splice(cancelledAt, 1)
|
|
2065
2071
|
const ids = session.acceptedInputIds || (session.acceptedInputIds = [])
|
|
2066
2072
|
const at = ids.indexOf(inputId)
|
|
2067
2073
|
if (at !== -1) ids.splice(at, 1)
|
|
@@ -2071,6 +2077,17 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2071
2077
|
}
|
|
2072
2078
|
}
|
|
2073
2079
|
|
|
2080
|
+
_recordCancelledInput(session, inputId) {
|
|
2081
|
+
if (!session || typeof inputId !== "string" || !inputId) return
|
|
2082
|
+
const ids = session.cancelledInputIds || (session.cancelledInputIds = [])
|
|
2083
|
+
const at = ids.indexOf(inputId)
|
|
2084
|
+
if (at !== -1) ids.splice(at, 1)
|
|
2085
|
+
ids.push(inputId)
|
|
2086
|
+
if (ids.length > ACCEPTED_INPUT_ID_RING_MAX) {
|
|
2087
|
+
ids.splice(0, ids.length - ACCEPTED_INPUT_ID_RING_MAX)
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2074
2091
|
/**
|
|
2075
2092
|
* 重複ガードの台帳から id を取り消す (2026-08-05)。
|
|
2076
2093
|
*
|
|
@@ -2103,6 +2120,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2103
2120
|
seq: session.queueStateSeq || 0,
|
|
2104
2121
|
generation: session.seq,
|
|
2105
2122
|
accepted: [...(session.acceptedInputIds || [])],
|
|
2123
|
+
cancelled: [...(session.cancelledInputIds || [])],
|
|
2106
2124
|
}
|
|
2107
2125
|
}
|
|
2108
2126
|
|
|
@@ -2121,6 +2139,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2121
2139
|
return false
|
|
2122
2140
|
}
|
|
2123
2141
|
const [item] = session.inputQueue.splice(index, 1)
|
|
2142
|
+
this._recordCancelledInput(session, item.inputId || item.deliveryId)
|
|
2124
2143
|
if (!item.acceptedAsQueued) {
|
|
2125
2144
|
const err = Object.assign(new Error("codex queued input cancelled"), {
|
|
2126
2145
|
reason: "cancelled",
|
package/src/main.mjs
CHANGED
|
@@ -59,6 +59,7 @@ import { UploadManager } from "./claude-upload.mjs"
|
|
|
59
59
|
import { requestSelfUninstall } from "./service-install.mjs"
|
|
60
60
|
import {
|
|
61
61
|
drainTaskNotificationEnvelopeDrops,
|
|
62
|
+
deleteSession,
|
|
62
63
|
fetchSessionHistory,
|
|
63
64
|
listSessions,
|
|
64
65
|
renameSession,
|
|
@@ -5032,6 +5033,42 @@ export async function dispatch(msg, ctx) {
|
|
|
5032
5033
|
})
|
|
5033
5034
|
return
|
|
5034
5035
|
}
|
|
5036
|
+
case "claude.session.delete": {
|
|
5037
|
+
const stream_id = msg.stream_id
|
|
5038
|
+
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
5039
|
+
const session_id = typeof msg.session_id === "string" ? msg.session_id : ""
|
|
5040
|
+
const request_id = typeof msg.request_id === "string" ? msg.request_id : undefined
|
|
5041
|
+
let profileId
|
|
5042
|
+
let result
|
|
5043
|
+
let stopped = 0
|
|
5044
|
+
if (!cwd) {
|
|
5045
|
+
result = { ok: false, error: "cwd required" }
|
|
5046
|
+
} else if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(session_id)) {
|
|
5047
|
+
result = { ok: false, error: "invalid session_id" }
|
|
5048
|
+
} else {
|
|
5049
|
+
const resolved = await resolveProjectsRoot(msg.profile_id)
|
|
5050
|
+
profileId = resolved.profileId
|
|
5051
|
+
try {
|
|
5052
|
+
const active = await getActiveProfile()
|
|
5053
|
+
stopped = ctx.claudeBridge && resolved.profileId === active.id
|
|
5054
|
+
? await ctx.claudeBridge.stopSessionForDeletion({ cwd, session_id })
|
|
5055
|
+
: 0
|
|
5056
|
+
result = await deleteSession({ cwd, session_id, projectsRoot: resolved.projectsRoot, logger: ctx.logger })
|
|
5057
|
+
} catch (err) {
|
|
5058
|
+
ctx.logger?.warn?.({ err: err?.message || String(err) }, "claude session delete stop failed")
|
|
5059
|
+
result = { ok: false, error: "delete failed" }
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
ctx.client.send({
|
|
5063
|
+
type: "claude.session.delete.response",
|
|
5064
|
+
request_id, stream_id, cwd, session_id,
|
|
5065
|
+
profile_id: profileId,
|
|
5066
|
+
ok: result.ok === true,
|
|
5067
|
+
stopped: result.ok ? stopped : undefined,
|
|
5068
|
+
error: result.ok ? undefined : result.error,
|
|
5069
|
+
})
|
|
5070
|
+
return
|
|
5071
|
+
}
|
|
5035
5072
|
case "profile.list": {
|
|
5036
5073
|
// Claude アカウント・プロファイル一覧と active を返す (Cockpit のアカウント切替UI用)。
|
|
5037
5074
|
// 各プロファイルの「最後にログインしていたアカウント」(<configDir>/.claude.json の
|