@cocorograph/hub-agent 0.8.53 → 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/claude-stream-bridge.mjs +7 -1
- package/src/codex-stream-bridge.mjs +145 -43
- package/src/main.mjs +2 -1
- package/src/sibling-inject.mjs +88 -20
package/package.json
CHANGED
|
@@ -40,13 +40,19 @@ import { nextSessionSeq } from "./session-seq.mjs"
|
|
|
40
40
|
* この cwd の capability トークンを乗せる。ディスクには書かず、プロセス環境変数
|
|
41
41
|
* としてのみ渡す (src/sibling-token.mjs のコメント参照)。
|
|
42
42
|
* @param {string} cwd
|
|
43
|
-
* @returns {{COCKPIT_SIBLING_TOKEN: string, COCKPIT_SIBLING_CWD: string
|
|
43
|
+
* @returns {NodeJS.ProcessEnv & {COCKPIT_SIBLING_TOKEN: string, COCKPIT_SIBLING_CWD: string,
|
|
44
|
+
* COCKPIT_DELEGATE_EVENT_ACK: string}}
|
|
44
45
|
*/
|
|
45
46
|
function siblingEnvFor(cwd) {
|
|
46
47
|
return {
|
|
47
48
|
...process.env,
|
|
48
49
|
COCKPIT_SIBLING_TOKEN: getOrCreateSiblingToken(cwd),
|
|
49
50
|
COCKPIT_SIBLING_CWD: cwd,
|
|
51
|
+
// hub-agent が spawn した Claude 配下 producer だけが delegate-event の受理 ACK を
|
|
52
|
+
// 要求するための capability。能力は spawn 時点で固定されるため、hub-agent を旧版へ
|
|
53
|
+
// ロールバックしても既存 producer は ACK を要求し続け、timeout し得る。
|
|
54
|
+
// 旧 bundle はこの値を無視し、従来どおり fire-and-forget のまま動く。
|
|
55
|
+
COCKPIT_DELEGATE_EVENT_ACK: "1",
|
|
50
56
|
// ask-sibling スキル (cockpit_orch.py) が「自分は claude/codex どちらか」を
|
|
51
57
|
// 環境変数から確実に判定できるようにする (ヒューリスティック不要)。
|
|
52
58
|
COCKPIT_SIBLING_SELF_KIND: "claude",
|
|
@@ -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
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -37,10 +37,13 @@
|
|
|
37
37
|
* - 安全な再起動: `{op:"restart"}` → `{ok:true, restart_pending:true}`。本文と token を持たず、
|
|
38
38
|
* 所有者限定の IPC directory/request file (0700/0600) だけを境界として扱う。
|
|
39
39
|
* 処理後は必ず削除し (成否問わず)、通常 IPC は呼び出し側が待ち受けられるよう
|
|
40
|
-
* `<request_id>.result.json` = `{ok, error?, delivered_to?, candidates?, live_sessions?, hint?,
|
|
40
|
+
* `<request_id>.result.json` = `{ok, error?, duplicate?, delivered_to?, candidates?, live_sessions?, hint?,
|
|
41
41
|
* candidates_nameable?}` を書く (`candidates_nameable` は曖昧系エラーのときのみ。CLI が
|
|
42
42
|
* 「候補を実行可能な `--to` として並べてよいか」を hint の文言に依存せず判定するための boolean)。
|
|
43
|
-
*
|
|
43
|
+
* 同一 request_id の再投入は二重処理防止で無視するため、意味上の再送は新しい transport ID を使う。
|
|
44
|
+
* `delegate-event` の `request_ack` は省略または literal true だけが正規入力。true のとき受理結果を
|
|
45
|
+
* 書き、不正値でも property があれば明示エラーを書く。未指定の旧 producer は fire-and-forget の
|
|
46
|
+
* ままなので、新旧 bundle / hub-agent のローリング互換を保つ。
|
|
44
47
|
*
|
|
45
48
|
* ⚠️ codex の `delivery_state` は 4 値ある (2026-08-05 に `queued` を追加):
|
|
46
49
|
* - `delivered` … userMessage echo まで確認済み (ok:true)
|
|
@@ -82,6 +85,7 @@ const SIBLING_SWEEP_DEBOUNCE_MS = 25
|
|
|
82
85
|
|
|
83
86
|
export const ACTIVE_DELEGATIONS_PER_SESSION_LIMIT = 100
|
|
84
87
|
export const DELEGATION_LEDGER_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
88
|
+
export const SIBLING_RESULT_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
85
89
|
const SIBLING_REQUEST_MAX_BYTES = 1024 * 1024
|
|
86
90
|
const DELEGATE_PROMPT_MAX_LENGTH = 64_000
|
|
87
91
|
const TERMINAL_DELEGATE_PROMPT_MAX_LENGTH = 2_000
|
|
@@ -491,8 +495,8 @@ export class SiblingInjectBridge {
|
|
|
491
495
|
}
|
|
492
496
|
}
|
|
493
497
|
|
|
494
|
-
_recordDelegateEvent(cwd, sessionId, event) {
|
|
495
|
-
this._pruneDelegations()
|
|
498
|
+
_recordDelegateEvent(cwd, sessionId, event, { prune = true } = {}) {
|
|
499
|
+
if (prune) this._pruneDelegations()
|
|
496
500
|
const sessionKey = this._delegationSessionKey(cwd, sessionId)
|
|
497
501
|
if (event.phase !== "started") {
|
|
498
502
|
const activeEntry = this._activeDelegations.get(sessionKey)?.get(event.delegation_id)
|
|
@@ -542,11 +546,40 @@ export class SiblingInjectBridge {
|
|
|
542
546
|
}
|
|
543
547
|
}
|
|
544
548
|
|
|
545
|
-
|
|
549
|
+
_delegateEventAcceptance(cwd, sessionId, event) {
|
|
546
550
|
this._pruneDelegations()
|
|
547
|
-
if (event.phase !== "started") return true
|
|
548
551
|
const sessionKey = this._delegationSessionKey(cwd, sessionId)
|
|
549
|
-
|
|
552
|
+
const terminal = this._terminalDelegations.get(sessionKey)?.get(event.delegation_id)
|
|
553
|
+
if (event.phase === "started") {
|
|
554
|
+
if (terminal) return "stale"
|
|
555
|
+
return this._activeDelegations.get(sessionKey)?.has(event.delegation_id)
|
|
556
|
+
? "duplicate"
|
|
557
|
+
: "accept"
|
|
558
|
+
}
|
|
559
|
+
if (!terminal) return "accept"
|
|
560
|
+
// terminal 同士は後から届いた実イベントを最新終端として上書きできる。
|
|
561
|
+
// 例: replay が PID 消失から stopped を合成した後に producer の completed が届く。
|
|
562
|
+
return terminal.terminalEvent?.phase === event.phase ? "duplicate" : "accept"
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
_liveClaudeSession(cwd, sessionId) {
|
|
566
|
+
const matches = describeLiveSessionsUnion(this._bridges(), {
|
|
567
|
+
cwd,
|
|
568
|
+
cliKind: "claude",
|
|
569
|
+
}).filter((session) => session.session_id === sessionId)
|
|
570
|
+
if (matches.length === 1) return { status: "delivered", session: matches[0] }
|
|
571
|
+
return {
|
|
572
|
+
status: matches.length > 1 ? "ambiguous" : "not_found",
|
|
573
|
+
...(matches.length ? { candidates: matches } : {}),
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_delegateTargetFailure(targetResult) {
|
|
578
|
+
return {
|
|
579
|
+
ok: false,
|
|
580
|
+
error: AMBIGUOUS_TARGET_ERROR,
|
|
581
|
+
...(targetResult?.candidates ? { candidates: targetResult.candidates } : {}),
|
|
582
|
+
}
|
|
550
583
|
}
|
|
551
584
|
|
|
552
585
|
async _delegateProcessAlive(event) {
|
|
@@ -699,7 +732,26 @@ export class SiblingInjectBridge {
|
|
|
699
732
|
return
|
|
700
733
|
}
|
|
701
734
|
for (const n of names) {
|
|
702
|
-
if (n.endsWith(".
|
|
735
|
+
if (n.endsWith(".result.json")) {
|
|
736
|
+
const fp = path.join(this.dir, n)
|
|
737
|
+
try {
|
|
738
|
+
const info = await lstat(fp)
|
|
739
|
+
const uid = process.getuid?.()
|
|
740
|
+
if (
|
|
741
|
+
!info.isSymbolicLink() &&
|
|
742
|
+
info.isFile() &&
|
|
743
|
+
uid != null &&
|
|
744
|
+
info.uid === uid &&
|
|
745
|
+
// 壁時計が前へ step すると保持期間が短くなるが、result は再配送の正本ではなく
|
|
746
|
+
// 呼び出し側が回収する一時 ACK なので、24時間経過扱いでの掃除を許容する。
|
|
747
|
+
info.mtimeMs < this._now() - SIBLING_RESULT_RETENTION_MS
|
|
748
|
+
) {
|
|
749
|
+
await unlink(fp)
|
|
750
|
+
}
|
|
751
|
+
} catch {
|
|
752
|
+
/* 同時に呼び出し側が回収済みなら no-op */
|
|
753
|
+
}
|
|
754
|
+
} else if (n.endsWith(".json")) {
|
|
703
755
|
await this._ingest(n, { startup }).catch((err) => {
|
|
704
756
|
this.logger?.warn({ err: err?.message, filename: n }, "sibling-inject sweep failed")
|
|
705
757
|
})
|
|
@@ -709,7 +761,8 @@ export class SiblingInjectBridge {
|
|
|
709
761
|
|
|
710
762
|
/**
|
|
711
763
|
* 要求ファイル 1 件を読み、トークン検証後に対象 cwd のセッションへ注入する。
|
|
712
|
-
* 成否によらず要求ファイルは削除する。delegate-event
|
|
764
|
+
* 成否によらず要求ファイルは削除する。delegate-event は request_ack:true または
|
|
765
|
+
* malformed な request_ack property がある場合だけ結果ファイルも書く。
|
|
713
766
|
* @param {string} filename `<request_id>.json`
|
|
714
767
|
*/
|
|
715
768
|
async _ingest(filename, { startup = false } = {}) {
|
|
@@ -729,8 +782,8 @@ export class SiblingInjectBridge {
|
|
|
729
782
|
// ⚠️ **結果ファイルを必ず書く**。ここで書かずに戻ると、結果をポーリングしている
|
|
730
783
|
// 呼び出し側 (cockpit_orch.py) には「タイムアウト」としか見えない。タイムアウトは
|
|
731
784
|
// 「不達とは限らない」と扱う契約なので、拒否した事実が呼び出し側で不達に化ける
|
|
732
|
-
// (=黙って捨てたのと同じになる)
|
|
733
|
-
//
|
|
785
|
+
// (=黙って捨てたのと同じになる)。ここでは本文を parse できず op / request_ack を
|
|
786
|
+
// 判別できないため無条件に書く。旧 producer 向けの余分な結果は stale 掃除の対象。
|
|
734
787
|
await this._writeResult(request_id, {
|
|
735
788
|
ok: false,
|
|
736
789
|
error: "request_too_large",
|
|
@@ -778,6 +831,7 @@ export class SiblingInjectBridge {
|
|
|
778
831
|
timestamp,
|
|
779
832
|
pid,
|
|
780
833
|
pid_started_at,
|
|
834
|
+
request_ack,
|
|
781
835
|
} = body || {}
|
|
782
836
|
// 連携カード種別 (Phase 2, 2026-07-23): 注入を受け手画面で専用カード表示させるためのタグ。
|
|
783
837
|
// 既知値 ('peer'|'orchestrator') のみ通し、未知値/未指定は undefined に落とす (未知の source を
|
|
@@ -787,6 +841,7 @@ export class SiblingInjectBridge {
|
|
|
787
841
|
const isList = op === "list"
|
|
788
842
|
const isExplicit = op === "inject"
|
|
789
843
|
const isDelegateEvent = op === "delegate-event"
|
|
844
|
+
const hasRequestAck = Object.prototype.hasOwnProperty.call(body || {}, "request_ack")
|
|
790
845
|
const isRestart = op === "restart"
|
|
791
846
|
let result
|
|
792
847
|
const badKind = target_cli_kind !== "claude" && target_cli_kind !== "codex"
|
|
@@ -803,6 +858,7 @@ export class SiblingInjectBridge {
|
|
|
803
858
|
(prompt !== undefined && typeof prompt !== "string") ||
|
|
804
859
|
(pid !== undefined && (!Number.isInteger(pid) || pid <= 0)) ||
|
|
805
860
|
(pid_started_at !== undefined && typeof pid_started_at !== "string") ||
|
|
861
|
+
(request_ack !== undefined && request_ack !== true) ||
|
|
806
862
|
(timestamp !== undefined &&
|
|
807
863
|
(typeof timestamp !== "string" || Number.isNaN(Date.parse(timestamp))))
|
|
808
864
|
const badBody = isDelegateEvent
|
|
@@ -892,21 +948,26 @@ export class SiblingInjectBridge {
|
|
|
892
948
|
}
|
|
893
949
|
// 墓標確認から配送成功時の台帳反映まで await を挟まない同期区間に閉じる。
|
|
894
950
|
// 配送失敗した started は台帳へ残さず、終端後に遅着した started は配送前に拒否する。
|
|
895
|
-
|
|
951
|
+
const acceptance = this._delegateEventAcceptance(cwd, target_session_id, event)
|
|
952
|
+
if (acceptance === "duplicate") {
|
|
953
|
+
// ACK は「過去に受理した」だけでなく、対象 stream が現在も live であることも示す。
|
|
954
|
+
// 台帳だけで成功を返すと、宛先消失後の retry を受理済みと誤認させる。
|
|
955
|
+
const live = this._liveClaudeSession(cwd, target_session_id)
|
|
956
|
+
result = live.status === "delivered"
|
|
957
|
+
? { ok: true, duplicate: true }
|
|
958
|
+
: this._delegateTargetFailure(live)
|
|
959
|
+
} else if (acceptance === "stale") {
|
|
896
960
|
result = { ok: false, error: "stale_delegate_event" }
|
|
897
961
|
} else {
|
|
898
962
|
const r = this.claudeBridge?.emitToStream
|
|
899
963
|
? this.claudeBridge.emitToStream(cwd, target_session_id, event)
|
|
900
964
|
: { status: "not_found" }
|
|
901
965
|
if (r.status === "delivered") {
|
|
902
|
-
|
|
966
|
+
// acceptance 直前に prune 済み。同じ同期区間で二重 prune しない。
|
|
967
|
+
this._recordDelegateEvent(cwd, target_session_id, event, { prune: false })
|
|
903
968
|
result = { ok: true, delivered_to: r.session }
|
|
904
969
|
} else {
|
|
905
|
-
result =
|
|
906
|
-
ok: false,
|
|
907
|
-
error: AMBIGUOUS_TARGET_ERROR,
|
|
908
|
-
...(r.candidates ? { candidates: r.candidates } : {}),
|
|
909
|
-
}
|
|
970
|
+
result = this._delegateTargetFailure(r)
|
|
910
971
|
}
|
|
911
972
|
}
|
|
912
973
|
}
|
|
@@ -1068,6 +1129,7 @@ export class SiblingInjectBridge {
|
|
|
1068
1129
|
delivery_state: result.delivery_state,
|
|
1069
1130
|
durable_state: result.durable_state,
|
|
1070
1131
|
reason: result.reason,
|
|
1132
|
+
duplicate: result.duplicate,
|
|
1071
1133
|
}
|
|
1072
1134
|
: {
|
|
1073
1135
|
error: result.error,
|
|
@@ -1077,7 +1139,9 @@ export class SiblingInjectBridge {
|
|
|
1077
1139
|
}),
|
|
1078
1140
|
},
|
|
1079
1141
|
result.ok
|
|
1080
|
-
? result.
|
|
1142
|
+
? result.duplicate
|
|
1143
|
+
? "sibling-inject duplicate acknowledged"
|
|
1144
|
+
: result.delivery_state === "delivered"
|
|
1081
1145
|
? "sibling-inject delivered"
|
|
1082
1146
|
: result.delivery_state === "queued"
|
|
1083
1147
|
? "sibling-inject queued"
|
|
@@ -1085,7 +1149,11 @@ export class SiblingInjectBridge {
|
|
|
1085
1149
|
: "sibling-inject failed",
|
|
1086
1150
|
)
|
|
1087
1151
|
// startup sweep 由来の restart には待受者がいない。孤児 result を残さない。
|
|
1088
|
-
if (!isDelegateEvent && !(isRestart && startup))
|
|
1152
|
+
if ((!isDelegateEvent || hasRequestAck) && !(isRestart && startup)) {
|
|
1153
|
+
// delegate ACK は「対象 Claude stream の内部 event 経路へ受理」までの保証。
|
|
1154
|
+
// ブラウザ側がイベントを登録・描画したことまでは保証しない。
|
|
1155
|
+
await this._writeResult(request_id, result)
|
|
1156
|
+
}
|
|
1089
1157
|
// 要求ファイルは処理後に必ず削除する (成否問わず、再処理させないため)。
|
|
1090
1158
|
try {
|
|
1091
1159
|
await unlink(fp)
|