@cocorograph/hub-agent 0.8.54 → 0.8.55
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/codex-stream-bridge.mjs +145 -43
- package/src/main.mjs +2 -1
package/package.json
CHANGED
|
@@ -235,6 +235,18 @@ const IDLE_DETACH_TTL_MS = readPositiveIntEnv(
|
|
|
235
235
|
7 * 24 * 60 * 60 * 1000,
|
|
236
236
|
)
|
|
237
237
|
|
|
238
|
+
// browser が最後に detach してから app-server を warm 保持する猶予。lastActivityMs は
|
|
239
|
+
// sibling inject や app-server 通知でも更新されるため、「画面から離れてどれだけ経ったか」の
|
|
240
|
+
// 判定には使えない。browserDetachedAtMs を独立して持ち、既存の protected 判定が解けた後だけ
|
|
241
|
+
// app-server(MCP 子孫を含む)を回収する。Codex は app-server 配下で MCP 子を eager 起動し、
|
|
242
|
+
// 子 thread / MCP 単体の unload RPC も無いため、画面不在 session を app-server 単位で cold 化する
|
|
243
|
+
// 必要がある。Claude SDK session にはこの eager MCP 子構造が無いので Codex 固有 env とする。
|
|
244
|
+
// 既存の 7 日 idle TTL / warm bounds は別軸で維持する。
|
|
245
|
+
const DETACHED_REAP_TTL_MS = readPositiveIntEnv(
|
|
246
|
+
"HUB_AGENT_CODEX_DETACHED_REAP_TTL_MS",
|
|
247
|
+
30 * 60 * 1000,
|
|
248
|
+
)
|
|
249
|
+
|
|
238
250
|
// 子⑤ (T06544): warm 保持の上限を「マシン全体の総セッション数」から「各 cwd K 本 × 同時 warm
|
|
239
251
|
// 保持する cwd 数 N」へ再定義(親 T06539「1 cwd 1 本化」に整合)。旧 WARM_POOL_MAX(総数 LRU=5)は
|
|
240
252
|
// 廃止し、境界を cwd 単位で定義する。1 セッション ≈ 120-140MB(node ラッパー + 本体 + MCP 子)
|
|
@@ -333,6 +345,16 @@ const DELIVERY_ECHO_TIMEOUT_MS = readPositiveIntEnv(
|
|
|
333
345
|
8 * 1000,
|
|
334
346
|
)
|
|
335
347
|
|
|
348
|
+
// sibling delivery が「受理済みで再送不要」と判断できる status。delivered は echo 確認済み、
|
|
349
|
+
// delivery_queued は FIFO 受理済み、delivery_unconfirmed は RPC 受理後に echo だけ未確認であり、
|
|
350
|
+
// いずれも画面不在中の実 traffic として cold 化窓を更新する。turn RPC 前に失敗して再送可能な
|
|
351
|
+
// delivery_failed は含めない。
|
|
352
|
+
const DELIVERY_WINDOW_REFRESH_STATUSES = new Set([
|
|
353
|
+
"delivered",
|
|
354
|
+
"delivery_queued",
|
|
355
|
+
"delivery_unconfirmed",
|
|
356
|
+
])
|
|
357
|
+
|
|
336
358
|
const CLIENT_INFO = { name: "cockpit-hub-agent", title: "Cockpit", version: "1.0.0" }
|
|
337
359
|
const WORKSPACE_DEPENDENCIES_CACHE_TTL_MS = 30_000
|
|
338
360
|
const WORKSPACE_DEPENDENCIES_FAILURE_TTL_MS = readPositiveIntEnv(
|
|
@@ -601,6 +623,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
601
623
|
logger,
|
|
602
624
|
clientFactory,
|
|
603
625
|
detachTtlMs,
|
|
626
|
+
detachedReapTtlMs,
|
|
604
627
|
warmCwdMax,
|
|
605
628
|
warmPerCwdMax,
|
|
606
629
|
stuckTurnTtlMs,
|
|
@@ -692,6 +715,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
692
715
|
|| preflightCodexArtifactTemplatePicker
|
|
693
716
|
// Stage 5: warm 保持の idle TTL(既定 7 日)。テストから短縮できるよう注入可能。
|
|
694
717
|
this._detachTtlMs = detachTtlMs ?? IDLE_DETACH_TTL_MS
|
|
718
|
+
// browser の最終 detach を起点にした短期 warm TTL(既定 30 分)。内部配送の活性では解除しない。
|
|
719
|
+
this._detachedReapTtlMs = detachedReapTtlMs ?? DETACHED_REAP_TTL_MS
|
|
695
720
|
// 子⑤ (T06544): 同時 warm 保持する cwd 数の上限 N(既定 20)。各 cwd は K 本に畳む。
|
|
696
721
|
// テストから小さい値を注入可能。⚠️ 正の整数以外(1.5 / Infinity / NaN)は既定へ戻して warn する。
|
|
697
722
|
// 1.5 が for の開始添字に入ると `list[1.5]` が undefined になり `_isProtected(undefined)` で
|
|
@@ -760,8 +785,10 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
760
785
|
*
|
|
761
786
|
* @returns {Promise<{resuming: boolean, threadId: string, history: object[],
|
|
762
787
|
* historyTruncated: boolean, historyStart?: number, historyTotal?: number,
|
|
788
|
+
* subagents?: object[],
|
|
763
789
|
* threadStatus: string|null, lastTurnStatus: string|null,
|
|
764
790
|
* turnActive: boolean}>}
|
|
791
|
+
* `subagents` の省略は snapshot 未提供(既存 UI 状態を維持)を意味し、空配列は権威的に0件を意味する。
|
|
765
792
|
*/
|
|
766
793
|
async attach({ stream_id, cwd, model, effort, approvalPolicy, sandbox, resumeThreadId, attachId }) {
|
|
767
794
|
if (this.sessions.has(stream_id)) {
|
|
@@ -791,6 +818,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
791
818
|
const r = await this._tryReattach(existing, stream_id)
|
|
792
819
|
timing.reattach_wait_ms = (timing.reattach_wait_ms ?? 0) + lap()
|
|
793
820
|
if (r.status === "reattached") {
|
|
821
|
+
const snapshot = this._warmReattachSnapshot(existing, r.info)
|
|
794
822
|
this._logAttachTiming({
|
|
795
823
|
stream_id,
|
|
796
824
|
cwd,
|
|
@@ -800,11 +828,12 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
800
828
|
ok: true,
|
|
801
829
|
resuming: true,
|
|
802
830
|
thread_op: null,
|
|
803
|
-
history_items:
|
|
831
|
+
history_items: snapshot.history.length,
|
|
832
|
+
history_truncated: snapshot.historyTruncated,
|
|
804
833
|
total_ms: Date.now() - attachStartedAt,
|
|
805
834
|
timing,
|
|
806
835
|
})
|
|
807
|
-
return
|
|
836
|
+
return snapshot
|
|
808
837
|
}
|
|
809
838
|
if (r.status === "superseded") {
|
|
810
839
|
this._logAttachTiming({
|
|
@@ -848,7 +877,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
848
877
|
const r = await this._tryReattach(existing, stream_id)
|
|
849
878
|
timing.reattach_wait_ms = (timing.reattach_wait_ms ?? 0) + lap()
|
|
850
879
|
if (r.status === "reattached") {
|
|
851
|
-
const
|
|
880
|
+
const snapshot = this._warmReattachSnapshot(existing, r.info)
|
|
852
881
|
this._logAttachTiming({
|
|
853
882
|
stream_id,
|
|
854
883
|
cwd,
|
|
@@ -858,27 +887,12 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
858
887
|
ok: true,
|
|
859
888
|
resuming: true,
|
|
860
889
|
thread_op: null,
|
|
861
|
-
history_items: history.length,
|
|
862
|
-
history_truncated:
|
|
890
|
+
history_items: snapshot.history.length,
|
|
891
|
+
history_truncated: snapshot.historyTruncated,
|
|
863
892
|
total_ms: Date.now() - attachStartedAt,
|
|
864
893
|
timing,
|
|
865
894
|
})
|
|
866
|
-
return
|
|
867
|
-
...r.info,
|
|
868
|
-
// 案1: reattach は履歴を返さない設計 (r.info.history=[]) だが、cwd reattach は remount
|
|
869
|
-
// 由来で表示がリセットされ得るため、live セッションが保持する高忠実度履歴を返す。
|
|
870
|
-
history,
|
|
871
|
-
historyTruncated: existing.historyTruncated === true,
|
|
872
|
-
...(history.length > 0
|
|
873
|
-
? {
|
|
874
|
-
historyStart: Number.isInteger(existing.historyStart) ? existing.historyStart : 0,
|
|
875
|
-
historyTotal: Number.isInteger(existing.historyTotal)
|
|
876
|
-
? existing.historyTotal
|
|
877
|
-
: history.length,
|
|
878
|
-
}
|
|
879
|
-
: {}),
|
|
880
|
-
subagents: this._subagentSnapshots(existing, history),
|
|
881
|
-
}
|
|
895
|
+
return snapshot
|
|
882
896
|
}
|
|
883
897
|
if (r.status === "superseded") {
|
|
884
898
|
this._logAttachTiming({
|
|
@@ -983,6 +997,13 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
983
997
|
// (busy/stuck)で決める。detach してもすぐには殺さず warm 保持する。
|
|
984
998
|
// ask-sibling 注入 (sendToCwd) は「新しい持ち主が付いた」とみなして false に戻す。
|
|
985
999
|
detachRequested: false,
|
|
1000
|
+
// 最後の browser subscriber が detach した時刻。実 browser reattach だけが null へ戻す。
|
|
1001
|
+
// sibling inject 等の内部配送では残し、画面不在の app-server を短期 TTL 後に回収できるようにする。
|
|
1002
|
+
browserDetachedAtMs: null,
|
|
1003
|
+
// 明示 codex.detach 後に同 stream の古い interval callback が到着しても、実 attach 前に
|
|
1004
|
+
// browser 復帰扱いへ戻さないための最小フラグ。死端末 GC 起因では false のままなので、
|
|
1005
|
+
// 本当に生存していた browser の keepalive は復帰シグナルとして使える。
|
|
1006
|
+
explicitBrowserDetachPendingAttach: false,
|
|
986
1007
|
dead: false,
|
|
987
1008
|
// Stage 5 reconcile(LRU / idle TTL / stuck)用の最終活性時刻。browser 入力と app-server
|
|
988
1009
|
// 通知 (ターン進捗) の両方で更新する。LRU は「最終活性が最も古い非 protected」を退避する。
|
|
@@ -1180,6 +1201,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1180
1201
|
// T06541 案1: cold attach で得た高忠実度履歴をセッションに保持する。以降ライブ item
|
|
1181
1202
|
// 完了ごとに追記し、cwd reattach 時にこれを返す(新プロセス thread/resume を再実行しない)。
|
|
1182
1203
|
// major#2: byte 予算判定を O(1) にするため概算 byte 合計も持つ (flatten は既に byte 上限適用済み)。
|
|
1204
|
+
// app-server reap 後の cold resume は rollout が正本になるため、未 rollout の空 fresh session や
|
|
1205
|
+
// in-memory の sibling source タグは復元できない。空 session の滞留を避けるため fresh 例外は作らない。
|
|
1183
1206
|
session.historyItems = history.slice()
|
|
1184
1207
|
session.historyBytes = history.reduce((n, it) => n + estimateItemBytes(it), 0)
|
|
1185
1208
|
session.historyTruncated = historyTruncated
|
|
@@ -1347,6 +1370,39 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1347
1370
|
return null
|
|
1348
1371
|
}
|
|
1349
1372
|
|
|
1373
|
+
/** attach-id / cwd の warm reattach ready payload を同じ shape で組み立てる。 */
|
|
1374
|
+
_warmReattachSnapshot(session, baseInfo) {
|
|
1375
|
+
// historyItems 自体が既存の item/byte 上限内なので、attach-id reconnect でも payload 上限を
|
|
1376
|
+
// 新たに広げない。frontend reducer は uuid で重複排除し、hidden 中の item 欠損だけを補う。
|
|
1377
|
+
const history = Array.isArray(session.historyItems) ? session.historyItems.slice() : []
|
|
1378
|
+
return {
|
|
1379
|
+
...baseInfo,
|
|
1380
|
+
history,
|
|
1381
|
+
historyTruncated: session.historyTruncated === true,
|
|
1382
|
+
...(history.length > 0
|
|
1383
|
+
? {
|
|
1384
|
+
historyStart: Number.isInteger(session.historyStart) ? session.historyStart : 0,
|
|
1385
|
+
historyTotal: Number.isInteger(session.historyTotal)
|
|
1386
|
+
? session.historyTotal
|
|
1387
|
+
: history.length,
|
|
1388
|
+
}
|
|
1389
|
+
: {}),
|
|
1390
|
+
...this._completeSubagentSnapshotField(session, history),
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* warm ready の subagents 三値契約。session.subagents が表示台帳の正本で、空なら権威的な []。
|
|
1396
|
+
* 1件でも started anchor が trim 等で history 外なら部分 snapshot を返さず、キー自体を省略して
|
|
1397
|
+
* frontend の既存表示を維持させる。全 record を表現できる場合だけ配列を返す。
|
|
1398
|
+
*/
|
|
1399
|
+
_completeSubagentSnapshotField(session, history) {
|
|
1400
|
+
const recordCount = session?.subagents?.size ?? 0
|
|
1401
|
+
if (recordCount === 0) return { subagents: [] }
|
|
1402
|
+
const snapshots = this._subagentSnapshots(session, history)
|
|
1403
|
+
return snapshots.length === recordCount ? { subagents: snapshots } : {}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1350
1406
|
/**
|
|
1351
1407
|
* T06541 案1: 完了 ThreadItem を高忠実度履歴 (session.historyItems) に追記する。
|
|
1352
1408
|
* 表示用バブル時刻 at_ms は completedAtMs (ms epoch) を採る(flattenThreadItems と同じ ms 単位。
|
|
@@ -1777,6 +1833,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1777
1833
|
this.sessions.set(newStreamId, session)
|
|
1778
1834
|
session.stream_id = newStreamId
|
|
1779
1835
|
session.detachRequested = false
|
|
1836
|
+
session.browserDetachedAtMs = null
|
|
1837
|
+
session.explicitBrowserDetachPendingAttach = false
|
|
1780
1838
|
session.lastActivityMs = now
|
|
1781
1839
|
this.logger?.info?.(
|
|
1782
1840
|
{
|
|
@@ -1793,15 +1851,16 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1793
1851
|
// 出したカードは remount/端末切替で失われ、応答が来ないとターンが永久 pending になる。新 stream へ
|
|
1794
1852
|
// 再 emit してカードを復元させる (frontend は request_id で重複排除するので二重化しない)。
|
|
1795
1853
|
this._replayPendingApprovals(session)
|
|
1796
|
-
//
|
|
1797
|
-
//
|
|
1854
|
+
// ここでは購読・権威状態だけを返す。呼び出し元 attach() が attach-id / cwd のどちらにも
|
|
1855
|
+
// _warmReattachSnapshot() を適用し、保持履歴・subagent snapshot を同じ ready shape へ揃える。
|
|
1856
|
+
// turnActive は session が保持する activeTurnId から権威的に決める。
|
|
1798
1857
|
return {
|
|
1799
1858
|
status: "reattached",
|
|
1800
1859
|
info: {
|
|
1801
1860
|
resuming: true,
|
|
1802
1861
|
threadId,
|
|
1803
1862
|
history: [],
|
|
1804
|
-
//
|
|
1863
|
+
// placeholder。保持履歴と cursor metadata は呼び出し元の共通 snapshot helper が上書きする。
|
|
1805
1864
|
historyTruncated: false,
|
|
1806
1865
|
threadStatus: session.activeTurnId ? "active" : "idle",
|
|
1807
1866
|
lastTurnStatus: null,
|
|
@@ -3760,19 +3819,36 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3760
3819
|
/**
|
|
3761
3820
|
* ブラウザ閲覧中の生存シグナル (codex.keepalive)。frontend が Codex チャットを開いて
|
|
3762
3821
|
* いる間だけ定期送信するため、これが届く = 「人が見ている」の観測可能な証跡になる。
|
|
3822
|
+
* frontend は ready 後に codex.keepalive の interval を開始する(現行60秒。WS pingの25秒とは別)。
|
|
3763
3823
|
* Stage 5: touch で lastActivityMs を更新すると LRU 先頭に留まるため、**閲覧中のセッションは
|
|
3764
3824
|
* 上限超過の LRU 退避対象にならず、長 idle TTL でも殺されない**(= 実質無期限)。ブラウザを
|
|
3765
3825
|
* 閉じれば keepalive も止まり、以降は detach 済み warm として LRU / 7 日 idle TTL に従う。
|
|
3766
3826
|
*
|
|
3767
|
-
*
|
|
3827
|
+
* 明示 detach 後は実 attach まで同 stream の残留 keepalive を false で無視する。一方、死端末 GC
|
|
3828
|
+
* 由来の detachRequested は live keepalive を復帰シグナルとして受け入れる。
|
|
3829
|
+
* @returns {boolean} セッションを touch できたら true(未知/dead/明示detach待ちは false)。
|
|
3768
3830
|
*/
|
|
3769
3831
|
keepalive({ stream_id }) {
|
|
3770
3832
|
const session = this.sessions.get(stream_id)
|
|
3771
3833
|
if (!session || session.dead) return false
|
|
3834
|
+
// 明示 detach の直前に既に timer queue へ入った callback は、detach 後にも同じ stream_id で
|
|
3835
|
+
// 到着し得る。クライアント契約では再表示時に必ず attach するため、その attach がこのフラグを
|
|
3836
|
+
// 解除するまでは古い keepalive として無視する。GC 起因の detach はフラグを立てない。
|
|
3837
|
+
if (session.explicitBrowserDetachPendingAttach) return false
|
|
3838
|
+
// keepalive は実 browser が閲覧中という証跡なので、detach 不達/遅延で残った warm 状態も解除する。
|
|
3839
|
+
// app-server 通知や sibling inject が通る _touch() ではこの browser 専用状態を解除しない。
|
|
3840
|
+
session.detachRequested = false
|
|
3841
|
+
session.browserDetachedAtMs = null
|
|
3772
3842
|
this._touch(session, stream_id)
|
|
3773
3843
|
return true
|
|
3774
3844
|
}
|
|
3775
3845
|
|
|
3846
|
+
/** accepted済みの新規 sibling traffic だけ browser-detached の cold 化窓を更新する。 */
|
|
3847
|
+
_shouldRefreshDetachWindow(result) {
|
|
3848
|
+
return result?.accepted_via !== "duplicate"
|
|
3849
|
+
&& DELIVERY_WINDOW_REFRESH_STATUSES.has(result?.status)
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3776
3852
|
/**
|
|
3777
3853
|
* グレースフル切断(Stage 5 + 案B T06542)。**すぐには reap しない**。
|
|
3778
3854
|
*
|
|
@@ -3812,6 +3888,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3812
3888
|
// 最後の 1 台が外れた: key / streamIds は残したまま(reconcile 到達性のため)warm 保持し、
|
|
3813
3889
|
// detachRequested を立てる。以降は周期 reconcile(LRU 上限 / 長 idle TTL / stuck)が reap を判断する。
|
|
3814
3890
|
session.detachRequested = true
|
|
3891
|
+
session.explicitBrowserDetachPendingAttach = true
|
|
3892
|
+
if (session.browserDetachedAtMs == null) session.browserDetachedAtMs = Date.now()
|
|
3815
3893
|
this.logger?.info?.(
|
|
3816
3894
|
{
|
|
3817
3895
|
stream_id,
|
|
@@ -3826,10 +3904,10 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3826
3904
|
/**
|
|
3827
3905
|
* 案B (T06542): 死端末 GC。多端末購読で、クラッシュ / タブ強制終了で codex.detach を送れずに
|
|
3828
3906
|
* 消えた購読端末(一定期間 input/permission/interrupt/keepalive の活性が無い stream_id)を購読集合
|
|
3829
|
-
* から掃除する。claude-stream-bridge.mjs の gcDeadTerminals
|
|
3830
|
-
*
|
|
3831
|
-
*
|
|
3832
|
-
* state loop 経由)ほか、テストから直接も呼べる。
|
|
3907
|
+
* から掃除する。claude-stream-bridge.mjs の gcDeadTerminals と同型。最後の 1 key は routing / reconcile
|
|
3908
|
+
* 到達性のため消さず、単端末を含め stale なら detachRequested と browserDetachedAtMs を立てる。
|
|
3909
|
+
* codex.detach 不達時の実効回収時間は dead-terminal TTL + detached reap TTL になる。
|
|
3910
|
+
* reconcileSessions の先頭から呼ばれる(main.mjs の state loop 経由)ほか、テストから直接も呼べる。
|
|
3833
3911
|
* @param {number} [ttlMs] 死端末とみなす無活性閾値(テスト用に上書き可)。
|
|
3834
3912
|
* @returns {number} GC した端末キー数。
|
|
3835
3913
|
*/
|
|
@@ -3837,8 +3915,6 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3837
3915
|
const now = Date.now()
|
|
3838
3916
|
let removed = 0
|
|
3839
3917
|
for (const session of this._liveSessions()) {
|
|
3840
|
-
// 複数端末が購読しているセッションのみ対象(1 台なら reconcile の idle TTL / LRU に任せる)。
|
|
3841
|
-
if (session.streamIds.size <= 1) continue
|
|
3842
3918
|
for (const sid of Array.from(session.streamIds)) {
|
|
3843
3919
|
// ⚠️ 不変条件: live session は常に stream key を 1 つ以上残す(reconcile 到達性)。全端末が
|
|
3844
3920
|
// 死端末でも最後の 1 個は消さず、下で detachRequested を立てて idle TTL reap に委ねる。
|
|
@@ -3862,7 +3938,10 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3862
3938
|
if (session.streamIds.size === 1) {
|
|
3863
3939
|
const [onlySid] = session.streamIds
|
|
3864
3940
|
const last = session.lastActivityByStream.get(onlySid) ?? 0
|
|
3865
|
-
if (now - last >= ttlMs)
|
|
3941
|
+
if (now - last >= ttlMs) {
|
|
3942
|
+
session.detachRequested = true
|
|
3943
|
+
if (session.browserDetachedAtMs == null) session.browserDetachedAtMs = now
|
|
3944
|
+
}
|
|
3866
3945
|
}
|
|
3867
3946
|
}
|
|
3868
3947
|
return removed
|
|
@@ -4002,17 +4081,23 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4002
4081
|
return { status: "ambiguous", candidates }
|
|
4003
4082
|
}
|
|
4004
4083
|
const best = tier[0]
|
|
4005
|
-
//
|
|
4006
|
-
//
|
|
4007
|
-
//
|
|
4084
|
+
// detachRequested=false は「内部配送先として現役」の routing 状態。browser 不在は
|
|
4085
|
+
// browserDetachedAtMs で別管理する。過去の 5 分 reap と違い、成功した sibling traffic ごとに
|
|
4086
|
+
// 30 分窓を更新し、その間の root/FIFO/delivery/command/subagent は _isProtected で守る。
|
|
4087
|
+
// 完全無活動が 30 分続いた後だけ app-server を cold 化する。not_found 時の cold 自動起動はしない。
|
|
4008
4088
|
best.detachRequested = false
|
|
4009
4089
|
this._touch(best)
|
|
4010
|
-
|
|
4090
|
+
const result = await this._deliverInjection(best, text, source, {
|
|
4011
4091
|
cwd,
|
|
4012
4092
|
selection: "sole-candidate",
|
|
4013
4093
|
candidates: this.describeLiveSessions(cwd).length,
|
|
4014
4094
|
request_id: requestId,
|
|
4015
4095
|
})
|
|
4096
|
+
// await 中に実 browser attach が入れば timestamp は null になる。その場合は再刻印しない。
|
|
4097
|
+
if (best.browserDetachedAtMs != null && this._shouldRefreshDetachWindow(result)) {
|
|
4098
|
+
best.browserDetachedAtMs = Date.now()
|
|
4099
|
+
}
|
|
4100
|
+
return result
|
|
4016
4101
|
}
|
|
4017
4102
|
|
|
4018
4103
|
/**
|
|
@@ -4066,16 +4151,21 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4066
4151
|
target = byThread[0] || null
|
|
4067
4152
|
}
|
|
4068
4153
|
if (!target) return { status: "not_found" }
|
|
4069
|
-
//
|
|
4070
|
-
//
|
|
4154
|
+
// detachRequested は内部配送の routing 状態、browserDetachedAtMs は画面不在の cold 化窓。
|
|
4155
|
+
// 成功した sibling traffic だけ後者を更新する(sendToCwd と同じ扱い)。
|
|
4071
4156
|
target.detachRequested = false
|
|
4072
4157
|
this._touch(target)
|
|
4073
|
-
|
|
4158
|
+
const result = await this._deliverInjection(target, text, source, {
|
|
4074
4159
|
cwd,
|
|
4075
4160
|
requested_id: id,
|
|
4076
4161
|
selection: bySid ? "stream_id" : "thread_id",
|
|
4077
4162
|
request_id: requestId,
|
|
4078
4163
|
})
|
|
4164
|
+
// await 中に実 browser attach が入れば timestamp は null になる。その場合は再刻印しない。
|
|
4165
|
+
if (target.browserDetachedAtMs != null && this._shouldRefreshDetachWindow(result)) {
|
|
4166
|
+
target.browserDetachedAtMs = Date.now()
|
|
4167
|
+
}
|
|
4168
|
+
return result
|
|
4079
4169
|
}
|
|
4080
4170
|
|
|
4081
4171
|
/**
|
|
@@ -4451,6 +4541,9 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4451
4541
|
cwd: session.cwd,
|
|
4452
4542
|
reason,
|
|
4453
4543
|
idle_ms: now - (session.lastActivityMs ?? now),
|
|
4544
|
+
browser_detached_for_ms: session.browserDetachedAtMs == null
|
|
4545
|
+
? null
|
|
4546
|
+
: Math.max(0, now - session.browserDetachedAtMs),
|
|
4454
4547
|
live_after: this._liveSessions().length,
|
|
4455
4548
|
},
|
|
4456
4549
|
"codex session reaped",
|
|
@@ -4572,13 +4665,16 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4572
4665
|
* Stage 5: セッションの周期 reconcile(state loop から ~5s 毎に実行)。以下を 1 パスで行う:
|
|
4573
4666
|
* 1. **stuck turn**: activeTurnId 固着だが app-server 通知が STUCK_TTL 途絶えた(ハング)→ reap
|
|
4574
4667
|
* (healthy な長ターンは reasoning/output delta で touch されるため該当しない)。
|
|
4575
|
-
* 2. **
|
|
4576
|
-
*
|
|
4668
|
+
* 2. **browser detached**: 最後の browser 離脱(または死端末 GC)後、短期 TTL(既定30分)を
|
|
4669
|
+
* 超えた非 protected session → app-server ごと reap。成功した sibling traffic は窓を更新する。
|
|
4670
|
+
* 3. **long idle**: 非 busy が idle TTL(既定 7 日)無活性 → reap(warm 保持の期限切れ)。
|
|
4671
|
+
* 4. **warm 境界**(子⑤ T06544): `_reconcileWarmBounds` で「各 cwd **K 本(現行の既定 3)**
|
|
4577
4672
|
* × 同時 warm cwd 数 N(既定 20)」に有界化。cwd 内で K 本を超えたぶんは最終活性が古い方から
|
|
4578
4673
|
* reap し、cwd 数超過は最古 cwd から reap する。
|
|
4579
4674
|
* (**旧仕様**は per-cwd=1 =「同一 cwd の重複は最新 1 本へ畳む」だった。1 cwd で複数会話を
|
|
4580
4675
|
* 同時に生かすため K へ一般化し、総量は総 live 上限 M(warm-bounds-total.mjs)で押さえる。)
|
|
4581
|
-
*
|
|
4676
|
+
* protected session は 1〜4 いずれの通常回収対象にもならない。ただし stuck turn は保護失効後、
|
|
4677
|
+
* browser-detached TTL より先に stuck-turn 理由で回収し、既存の診断意味を維持する。
|
|
4582
4678
|
*
|
|
4583
4679
|
* 旧 `gcAbandonedSessions`(1h 無活性一律 reap)を置換。detach 済み warm セッションを 1h で
|
|
4584
4680
|
* 殺していた挙動を廃し、上限+長 TTL に一本化した。main.mjs の state loop 呼び出しは
|
|
@@ -4597,15 +4693,21 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4597
4693
|
for (const session of this._liveSessions()) {
|
|
4598
4694
|
if (this._isProtected(session, now)) continue
|
|
4599
4695
|
const idle = now - (session.lastActivityMs ?? now)
|
|
4696
|
+
const browserDetachedFor = session.browserDetachedAtMs == null
|
|
4697
|
+
? null
|
|
4698
|
+
: now - session.browserDetachedAtMs
|
|
4600
4699
|
if (session.activeTurnId && idle > this._stuckTurnTtlMs) {
|
|
4601
4700
|
this._reapSession(session, now, "stuck-turn")
|
|
4602
4701
|
reaped += 1
|
|
4702
|
+
} else if (browserDetachedFor !== null && browserDetachedFor > this._detachedReapTtlMs) {
|
|
4703
|
+
this._reapSession(session, now, "browser-detached-ttl")
|
|
4704
|
+
reaped += 1
|
|
4603
4705
|
} else if (!session.activeTurnId && idle > this._detachTtlMs) {
|
|
4604
4706
|
this._reapSession(session, now, "idle-ttl")
|
|
4605
4707
|
reaped += 1
|
|
4606
4708
|
}
|
|
4607
4709
|
}
|
|
4608
|
-
//
|
|
4710
|
+
// 4: warm 境界(per-cwd=K / N-cwd LRU)を適用(stuck/idle reap 後の生存数で判定)。
|
|
4609
4711
|
reaped += this._reconcileWarmBounds(now, "warm-bounds")
|
|
4610
4712
|
return reaped
|
|
4611
4713
|
}
|
package/src/main.mjs
CHANGED
|
@@ -4702,7 +4702,8 @@ export async function dispatch(msg, ctx) {
|
|
|
4702
4702
|
case "codex.keepalive":
|
|
4703
4703
|
// ブラウザが Codex チャットを開いている間の生存シグナル。
|
|
4704
4704
|
// 閲覧中に gcAbandonedSessions (既定 1h 無通信) が
|
|
4705
|
-
//
|
|
4705
|
+
// 誤爆するのを防ぐ。未知/dead stream_id と、明示 detach 後に残った旧 interval callback は
|
|
4706
|
+
// attach まで false(無害な no-op)。死端末 GC 起因の detach は live keepalive で復帰できる。
|
|
4706
4707
|
if (!ctx.codexBridge) return
|
|
4707
4708
|
ctx.codexBridge.keepalive({ stream_id: msg.stream_id })
|
|
4708
4709
|
return
|