@cocorograph/hub-agent 0.8.48 → 0.8.50
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/bin/hub-agent.mjs +36 -7
- package/package.json +2 -2
- package/src/claude-stream-bridge.mjs +18 -5
- package/src/codex-artifact-template-picker.mjs +198 -0
- package/src/codex-dynamic-tools.mjs +191 -0
- package/src/codex-stream-bridge.mjs +405 -10
- package/src/codex-workspace-dependencies.mjs +331 -0
- package/src/main.mjs +174 -4
- package/src/service-install.mjs +51 -17
- package/src/sibling-inject.mjs +177 -23
package/src/sibling-inject.mjs
CHANGED
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
* postinstall 等) はどの cwd のトークンも持たないため排除される核心は維持。ターゲット
|
|
21
21
|
* 解決は引き続き要求の cwd でスコープする (誤 cwd 着弾・別 workspace 情報漏洩を防ぐ)。
|
|
22
22
|
* トークンが一致しない要求は無視し (実行せず削除)、warn ログのみ残す。
|
|
23
|
+
* 例外は同一UIDのサービス再起動 control (`op:"restart"`) だけで、本文を持たず、所有者限定
|
|
24
|
+
* directory/file (0700/0600) を境界にする。他の op の token 要件は緩めない。
|
|
23
25
|
*
|
|
24
|
-
* 契約: request file `<request_id>.json` は op
|
|
26
|
+
* 契約: request file `<request_id>.json` は op で用途を明示する (2026-07-19 / T06271):
|
|
25
27
|
* - 通常注入 (op 無し): `{cwd, target_cli_kind, text, token}`
|
|
26
28
|
* cwd 内が 1 本なら注入、複数なら黙って推測せず `ambiguous_target` + 候補一覧。
|
|
27
29
|
* - 明示注入: `{op:"inject", cwd, target_cli_kind, token, target_session_id, message}`
|
|
@@ -32,6 +34,8 @@
|
|
|
32
34
|
* - 一覧: `{op:"list", cwd, target_cli_kind, token}` → `{ok, live_sessions}` (--list 用)。
|
|
33
35
|
* - 全一覧: `{op:"list-all", token}` → `{ok, live_sessions}` (全 cwd・両 CLI。クロス cwd
|
|
34
36
|
* 宛先探索用、2026-07-23。cwd/target_cli_kind 不要、token のみ必須)。
|
|
37
|
+
* - 安全な再起動: `{op:"restart"}` → `{ok:true, restart_pending:true}`。本文と token を持たず、
|
|
38
|
+
* 所有者限定の IPC directory/request file (0700/0600) だけを境界として扱う。
|
|
35
39
|
* 処理後は必ず削除し (成否問わず)、通常 IPC は呼び出し側が待ち受けられるよう
|
|
36
40
|
* `<request_id>.result.json` = `{ok, error?, delivered_to?, candidates?, live_sessions?, hint?,
|
|
37
41
|
* candidates_nameable?}` を書く (`candidates_nameable` は曖昧系エラーのときのみ。CLI が
|
|
@@ -60,8 +64,9 @@
|
|
|
60
64
|
*/
|
|
61
65
|
|
|
62
66
|
import { watch } from "node:fs"
|
|
63
|
-
import { chmod, lstat, mkdir, readFile, writeFile, rename,
|
|
67
|
+
import { chmod, lstat, mkdir, readFile, writeFile, rename, unlink, readdir } from "node:fs/promises"
|
|
64
68
|
import { execFile as execFileCallback } from "node:child_process"
|
|
69
|
+
import { randomUUID } from "node:crypto"
|
|
65
70
|
import { promisify } from "node:util"
|
|
66
71
|
import path from "node:path"
|
|
67
72
|
|
|
@@ -71,6 +76,10 @@ import { describeLiveSessionsUnion } from "./live-sessions.mjs"
|
|
|
71
76
|
export const SIBLING_REQUESTS_DIR =
|
|
72
77
|
process.env.COCKPIT_SIBLING_REQUESTS_DIR || "/tmp/cockpit_sibling_requests"
|
|
73
78
|
|
|
79
|
+
const RESTART_REQUEST_TIMEOUT_MS = 5_000
|
|
80
|
+
const RESTART_RESULT_POLL_MS = 20
|
|
81
|
+
const SIBLING_SWEEP_DEBOUNCE_MS = 25
|
|
82
|
+
|
|
74
83
|
export const ACTIVE_DELEGATIONS_PER_SESSION_LIMIT = 100
|
|
75
84
|
export const DELEGATION_LEDGER_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
76
85
|
const SIBLING_REQUEST_MAX_BYTES = 1024 * 1024
|
|
@@ -155,6 +164,69 @@ export async function defaultPidStartedAt(pid, exec = execFile) {
|
|
|
155
164
|
return normalizeProcessStartedAt(stdout)
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
async function assertOwnedIpcPath(fp, { type, mode }) {
|
|
168
|
+
const info = await lstat(fp)
|
|
169
|
+
if (info.isSymbolicLink()) throw new Error(`${type} must not be a symbolic link`)
|
|
170
|
+
if (type === "IPC directory" ? !info.isDirectory() : !info.isFile()) {
|
|
171
|
+
throw new Error(`${type} has an unexpected file type`)
|
|
172
|
+
}
|
|
173
|
+
const uid = process.getuid?.()
|
|
174
|
+
if (uid == null || info.uid !== uid) throw new Error(`${type} is not owned by the current user`)
|
|
175
|
+
if ((info.mode & 0o777) !== mode) {
|
|
176
|
+
throw new Error(`${type} permissions must be ${mode.toString(8).padStart(4, "0")}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 所有者限定ファイル IPC で、稼働中 daemon へ deferred restart を要求する。
|
|
182
|
+
* restart op だけは capability token を使わず、既存 IPC の所有者・mode を認可境界にする。
|
|
183
|
+
*/
|
|
184
|
+
export async function requestDeferredRestart({
|
|
185
|
+
dir = SIBLING_REQUESTS_DIR,
|
|
186
|
+
timeoutMs = RESTART_REQUEST_TIMEOUT_MS,
|
|
187
|
+
pollMs = RESTART_RESULT_POLL_MS,
|
|
188
|
+
} = {}) {
|
|
189
|
+
await assertOwnedIpcPath(dir, { type: "IPC directory", mode: 0o700 })
|
|
190
|
+
const requestId = `restart-${process.pid}-${randomUUID()}`
|
|
191
|
+
const requestPath = path.join(dir, `${requestId}.json`)
|
|
192
|
+
const resultPath = path.join(dir, `${requestId}.result.json`)
|
|
193
|
+
const tmpPath = `${requestPath}.tmp`
|
|
194
|
+
try {
|
|
195
|
+
await writeFile(tmpPath, JSON.stringify({ op: "restart" }), { mode: 0o600, flag: "wx" })
|
|
196
|
+
await rename(tmpPath, requestPath)
|
|
197
|
+
const deadline = Date.now() + timeoutMs
|
|
198
|
+
while (Date.now() < deadline) {
|
|
199
|
+
try {
|
|
200
|
+
await assertOwnedIpcPath(resultPath, { type: "IPC result", mode: 0o600 })
|
|
201
|
+
const result = JSON.parse(await readFile(resultPath, "utf-8"))
|
|
202
|
+
if (!result?.ok) {
|
|
203
|
+
const daemonError = result?.error || "unknown"
|
|
204
|
+
const rejected = new Error(`daemon rejected deferred restart: ${daemonError}`)
|
|
205
|
+
// filesystem ENOENT 等とdaemon契約エラーのnamespaceを分離する。
|
|
206
|
+
rejected.code = "restart_rejected"
|
|
207
|
+
rejected.daemonError = daemonError
|
|
208
|
+
throw rejected
|
|
209
|
+
}
|
|
210
|
+
return result
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err?.code !== "ENOENT") throw err
|
|
213
|
+
}
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
215
|
+
}
|
|
216
|
+
const timeout = new Error(
|
|
217
|
+
"deferred restart acknowledgement timed out. If the daemon was just starting, the request " +
|
|
218
|
+
"may have been discarded; wait a few seconds and retry. It may also already have been accepted, " +
|
|
219
|
+
"so check the daemon log before deciding whether interruption with --force is acceptable.",
|
|
220
|
+
)
|
|
221
|
+
timeout.code = "restart_timeout"
|
|
222
|
+
throw timeout
|
|
223
|
+
} finally {
|
|
224
|
+
await unlink(tmpPath).catch(() => {})
|
|
225
|
+
await unlink(requestPath).catch(() => {})
|
|
226
|
+
await unlink(resultPath).catch(() => {})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
158
230
|
const DELIVERY_UNCONFIRMED_HINT =
|
|
159
231
|
"turn RPC は受理されましたが userMessage echo を確認できませんでした。既に届いている可能性があり、" +
|
|
160
232
|
"再送すると重複配送になります。再送前に対象セッションの受信状態または rollout 監査を確認してください。"
|
|
@@ -337,7 +409,8 @@ export class SiblingInjectBridge {
|
|
|
337
409
|
/**
|
|
338
410
|
* @param {{dir?: string, claudeBridge?: object, codexBridge?: object, logger?: object,
|
|
339
411
|
* now?: () => number, processKill?: (pid: number, signal: number) => void,
|
|
340
|
-
* pidStartedAt?: (pid: number) => Promise<string
|
|
412
|
+
* pidStartedAt?: (pid: number) => Promise<string>, onRestartRequested?: () => object,
|
|
413
|
+
* isInputBlocked?: () => boolean, watchFn?: typeof watch}} [opts]
|
|
341
414
|
*/
|
|
342
415
|
constructor({
|
|
343
416
|
dir = SIBLING_REQUESTS_DIR,
|
|
@@ -347,6 +420,9 @@ export class SiblingInjectBridge {
|
|
|
347
420
|
now = Date.now,
|
|
348
421
|
processKill = process.kill.bind(process),
|
|
349
422
|
pidStartedAt = defaultPidStartedAt,
|
|
423
|
+
onRestartRequested,
|
|
424
|
+
isInputBlocked,
|
|
425
|
+
watchFn = watch,
|
|
350
426
|
} = {}) {
|
|
351
427
|
this.dir = dir
|
|
352
428
|
this.claudeBridge = claudeBridge
|
|
@@ -355,15 +431,46 @@ export class SiblingInjectBridge {
|
|
|
355
431
|
/** @type {Set<string>} 二重処理防止 (request_id) */
|
|
356
432
|
this._seen = new Set()
|
|
357
433
|
this._watcher = null
|
|
434
|
+
this._sweepTimer = null
|
|
358
435
|
this._now = now
|
|
359
436
|
this._processKill = processKill
|
|
360
437
|
this._pidStartedAt = pidStartedAt
|
|
438
|
+
this._onRestartRequested = onRestartRequested
|
|
439
|
+
this._isInputBlocked = isInputBlocked
|
|
440
|
+
this._watchFn = watchFn
|
|
441
|
+
this._newTurnInFlight = 0
|
|
361
442
|
/** @type {Map<string, Map<string, {event: object, recordedAt: number}>>} cwd+session ごとの進行中委譲。 */
|
|
362
443
|
this._activeDelegations = new Map()
|
|
363
444
|
/** @type {Map<string, Map<string, {startedEvent?: object, terminalEvent: object, startedAt?: number, recordedAt: number}>>} 終了済み委譲の再送情報を兼ねた墓標。 */
|
|
364
445
|
this._terminalDelegations = new Map()
|
|
365
446
|
}
|
|
366
447
|
|
|
448
|
+
/** restart coordinator が通常/明示注入の bridge await 中を観測する。 */
|
|
449
|
+
hasActiveWork() {
|
|
450
|
+
return this._newTurnInFlight > 0
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async _withNewTurnInFlight(operation) {
|
|
454
|
+
this._newTurnInFlight += 1
|
|
455
|
+
try {
|
|
456
|
+
return await operation()
|
|
457
|
+
} finally {
|
|
458
|
+
this._newTurnInFlight -= 1
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async _sendNewTurn(operation, fields) {
|
|
463
|
+
try {
|
|
464
|
+
return await this._withNewTurnInFlight(operation)
|
|
465
|
+
} catch (err) {
|
|
466
|
+
this.logger?.error?.(
|
|
467
|
+
{ ...fields, err: err?.message, stack: err?.stack },
|
|
468
|
+
"sibling-inject bridge delivery failed",
|
|
469
|
+
)
|
|
470
|
+
return { status: "internal_error" }
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
367
474
|
_delegationSessionKey(cwd, sessionId) {
|
|
368
475
|
return `${cwd}\u0000${sessionId}`
|
|
369
476
|
}
|
|
@@ -551,6 +658,7 @@ export class SiblingInjectBridge {
|
|
|
551
658
|
|
|
552
659
|
/** 監視ディレクトリを作成し、既存要求を sweep してから fs.watch を張る。 */
|
|
553
660
|
async start() {
|
|
661
|
+
this.stop()
|
|
554
662
|
try {
|
|
555
663
|
await this._prepareDir()
|
|
556
664
|
} catch (err) {
|
|
@@ -560,14 +668,19 @@ export class SiblingInjectBridge {
|
|
|
560
668
|
)
|
|
561
669
|
return
|
|
562
670
|
}
|
|
563
|
-
await this._sweep()
|
|
671
|
+
await this._sweep({ startup: true })
|
|
564
672
|
try {
|
|
565
|
-
this._watcher =
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
673
|
+
this._watcher = this._watchFn(this.dir, () => {
|
|
674
|
+
// fs.watch のfilenameはOS都合で欠落・統合されうる。tmp作成とrenameを
|
|
675
|
+
// 短くまとめ、通知された1件でディレクトリ全体を正本として再走査する。
|
|
676
|
+
if (this._sweepTimer) clearTimeout(this._sweepTimer)
|
|
677
|
+
this._sweepTimer = setTimeout(() => {
|
|
678
|
+
this._sweepTimer = null
|
|
679
|
+
this._sweep().catch((err) => {
|
|
680
|
+
this.logger?.warn({ err: err?.message }, "sibling-inject event sweep failed")
|
|
569
681
|
})
|
|
570
|
-
}
|
|
682
|
+
}, SIBLING_SWEEP_DEBOUNCE_MS)
|
|
683
|
+
this._sweepTimer.unref?.()
|
|
571
684
|
})
|
|
572
685
|
} catch (err) {
|
|
573
686
|
this.logger?.warn(
|
|
@@ -578,7 +691,7 @@ export class SiblingInjectBridge {
|
|
|
578
691
|
}
|
|
579
692
|
|
|
580
693
|
/** 起動時 / 取りこぼし保険: dir 内の全要求ファイルを読む。 */
|
|
581
|
-
async _sweep() {
|
|
694
|
+
async _sweep({ startup = false } = {}) {
|
|
582
695
|
let names = []
|
|
583
696
|
try {
|
|
584
697
|
names = await readdir(this.dir)
|
|
@@ -587,7 +700,7 @@ export class SiblingInjectBridge {
|
|
|
587
700
|
}
|
|
588
701
|
for (const n of names) {
|
|
589
702
|
if (n.endsWith(".json") && !n.endsWith(".result.json")) {
|
|
590
|
-
await this._ingest(n).catch((err) => {
|
|
703
|
+
await this._ingest(n, { startup }).catch((err) => {
|
|
591
704
|
this.logger?.warn({ err: err?.message, filename: n }, "sibling-inject sweep failed")
|
|
592
705
|
})
|
|
593
706
|
}
|
|
@@ -599,17 +712,18 @@ export class SiblingInjectBridge {
|
|
|
599
712
|
* 成否によらず要求ファイルは削除する。delegate-event 以外は結果ファイルも書く。
|
|
600
713
|
* @param {string} filename `<request_id>.json`
|
|
601
714
|
*/
|
|
602
|
-
async _ingest(filename) {
|
|
715
|
+
async _ingest(filename, { startup = false } = {}) {
|
|
603
716
|
const request_id = filename.slice(0, -".json".length)
|
|
604
717
|
if (this._seen.has(request_id)) return
|
|
605
718
|
this._seen.add(request_id)
|
|
606
719
|
const fp = path.join(this.dir, filename)
|
|
607
720
|
let body
|
|
721
|
+
let requestInfo
|
|
608
722
|
try {
|
|
609
|
-
|
|
610
|
-
if (
|
|
723
|
+
requestInfo = await lstat(fp)
|
|
724
|
+
if (requestInfo.size > SIBLING_REQUEST_MAX_BYTES) {
|
|
611
725
|
this.logger?.warn(
|
|
612
|
-
{ filename, size:
|
|
726
|
+
{ filename, size: requestInfo.size, max_size: SIBLING_REQUEST_MAX_BYTES },
|
|
613
727
|
"sibling-inject: request file rejected because it exceeds the size limit",
|
|
614
728
|
)
|
|
615
729
|
// ⚠️ **結果ファイルを必ず書く**。ここで書かずに戻ると、結果をポーリングしている
|
|
@@ -620,7 +734,7 @@ export class SiblingInjectBridge {
|
|
|
620
734
|
await this._writeResult(request_id, {
|
|
621
735
|
ok: false,
|
|
622
736
|
error: "request_too_large",
|
|
623
|
-
size:
|
|
737
|
+
size: requestInfo.size,
|
|
624
738
|
max_size: SIBLING_REQUEST_MAX_BYTES,
|
|
625
739
|
})
|
|
626
740
|
try {
|
|
@@ -637,10 +751,11 @@ export class SiblingInjectBridge {
|
|
|
637
751
|
return
|
|
638
752
|
}
|
|
639
753
|
|
|
640
|
-
//
|
|
754
|
+
// 注入・一覧・control の契約形を op で判別する:
|
|
641
755
|
// - op:"list" → 候補列挙 (top-level text 無し)
|
|
642
756
|
// - op:"inject" → 明示宛先 (target_session_id + message、top-level text は **持たない**)
|
|
643
757
|
// - op 無し → 通常 (cwd 最新、top-level text あり) ← 旧 agent と後方互換
|
|
758
|
+
// - op:"restart" → 本文なしの deferred restart control
|
|
644
759
|
// ⚠️ 明示宛先を「op:inject + message (top-level text 無し)」にするのが fail-closed の肝
|
|
645
760
|
// (T06271 レビュー major①): 旧 hub-agent は `!text` で invalid_request を返すため、
|
|
646
761
|
// 未知フィールドを無視して sendToCwd 推測注入する事故が **単一リクエストで原理的に
|
|
@@ -672,6 +787,7 @@ export class SiblingInjectBridge {
|
|
|
672
787
|
const isList = op === "list"
|
|
673
788
|
const isExplicit = op === "inject"
|
|
674
789
|
const isDelegateEvent = op === "delegate-event"
|
|
790
|
+
const isRestart = op === "restart"
|
|
675
791
|
let result
|
|
676
792
|
const badKind = target_cli_kind !== "claude" && target_cli_kind !== "codex"
|
|
677
793
|
const delegatePhases = new Set(["started", "completed", "failed", "stopped"])
|
|
@@ -696,7 +812,29 @@ export class SiblingInjectBridge {
|
|
|
696
812
|
: isExplicit
|
|
697
813
|
? !target_session_id || !message // 明示は id と message 必須 (空指定 fail-open を防ぐ)
|
|
698
814
|
: !text // 通常は top-level text 必須 (=旧契約)
|
|
699
|
-
if (
|
|
815
|
+
if (isRestart) {
|
|
816
|
+
// restart は同一ユーザーが所有する 0700/0600 IPC だけを認可境界にする専用 control op。
|
|
817
|
+
// 本文・token・cwd 等を許すと他 op の契約へ認証緩和が波及するため、op 単独だけを受理する。
|
|
818
|
+
const uid = process.getuid?.()
|
|
819
|
+
const unsafeRequestFile =
|
|
820
|
+
requestInfo.isSymbolicLink() ||
|
|
821
|
+
!requestInfo.isFile() ||
|
|
822
|
+
uid == null ||
|
|
823
|
+
requestInfo.uid !== uid ||
|
|
824
|
+
(requestInfo.mode & 0o777) !== 0o600
|
|
825
|
+
const hasBody = Object.keys(body || {}).some((key) => key !== "op")
|
|
826
|
+
if (unsafeRequestFile || hasBody) {
|
|
827
|
+
result = { ok: false, error: "invalid_request" }
|
|
828
|
+
} else if (startup) {
|
|
829
|
+
// 前プロセスがACK前に落ちて残した要求を、次のdaemon起動時に勝手に実行しない。
|
|
830
|
+
// live watcher到着分だけがrestartを開始できる。
|
|
831
|
+
result = { ok: false, error: "restart_stale" }
|
|
832
|
+
} else if (typeof this._onRestartRequested !== "function") {
|
|
833
|
+
result = { ok: false, error: "restart_unavailable" }
|
|
834
|
+
} else {
|
|
835
|
+
result = this._onRestartRequested()
|
|
836
|
+
}
|
|
837
|
+
} else if (isListAll) {
|
|
700
838
|
// op:"list-all" (2026-07-23 統一再設計 1-2: クロス cwd 宛先探索): 全 cwd・両 CLI の
|
|
701
839
|
// ライブセッションを列挙する。cwd / target_cli_kind / 本文は不要 (token のみ必須)。
|
|
702
840
|
// 認可は verifySiblingTokenAny (同一ユーザーの spawn 済みセッションであれば全 cwd を
|
|
@@ -783,15 +921,23 @@ export class SiblingInjectBridge {
|
|
|
783
921
|
cliKind: target_cli_kind,
|
|
784
922
|
}),
|
|
785
923
|
}
|
|
924
|
+
} else if (this._isInputBlocked?.()) {
|
|
925
|
+
// token検証後にのみ状態を返す。delegate-event/list/controlは新turnを作らないため通す。
|
|
926
|
+
result = { ok: false, error: "restart_pending" }
|
|
786
927
|
} else if (isExplicit) {
|
|
787
928
|
// 明示宛先: session_id (claude) / stream_id|thread_id (codex) で cwd 一致下に確定解決。
|
|
788
929
|
// sendToStream は {status:"delivered"|"not_found"|"ambiguous"} を返す。
|
|
789
930
|
// request_id を渡すのは、bridge 側が配送 ID を **決定的に** 導出して同一依頼の再処理を
|
|
790
931
|
// 重複ガードで止められるようにするため (claude bridge は余分な引数を無視する)。
|
|
791
932
|
const r = bridge?.sendToStream
|
|
792
|
-
? await
|
|
933
|
+
? await this._sendNewTurn(
|
|
934
|
+
() => bridge.sendToStream(cwd, target_session_id, message, injectSource, request_id),
|
|
935
|
+
{ request_id, cwd, target_cli_kind, op: "inject" },
|
|
936
|
+
)
|
|
793
937
|
: { status: "not_found" }
|
|
794
|
-
if (r.status === "
|
|
938
|
+
if (r.status === "internal_error") {
|
|
939
|
+
result = { ok: false, error: "internal_error" }
|
|
940
|
+
} else if (r.status === "delivered") {
|
|
795
941
|
result = {
|
|
796
942
|
ok: true,
|
|
797
943
|
delivered_to: r.session,
|
|
@@ -857,9 +1003,14 @@ export class SiblingInjectBridge {
|
|
|
857
1003
|
// あった。sendToCwd を status 返却にして解決と配送を 1 呼び出しに統合し、delivered_to は
|
|
858
1004
|
// **実配送先 (r.session)** を返す。
|
|
859
1005
|
const r = bridge?.sendToCwd
|
|
860
|
-
? await
|
|
1006
|
+
? await this._sendNewTurn(
|
|
1007
|
+
() => bridge.sendToCwd(cwd, text, injectSource, request_id),
|
|
1008
|
+
{ request_id, cwd, target_cli_kind, op: "normal" },
|
|
1009
|
+
)
|
|
861
1010
|
: { status: "not_found" }
|
|
862
|
-
if (r.status === "
|
|
1011
|
+
if (r.status === "internal_error") {
|
|
1012
|
+
result = { ok: false, error: "internal_error" }
|
|
1013
|
+
} else if (r.status === "delivered") {
|
|
863
1014
|
result = {
|
|
864
1015
|
ok: true,
|
|
865
1016
|
delivered_to: r.session ?? null,
|
|
@@ -933,7 +1084,8 @@ export class SiblingInjectBridge {
|
|
|
933
1084
|
: "sibling-inject dispatched"
|
|
934
1085
|
: "sibling-inject failed",
|
|
935
1086
|
)
|
|
936
|
-
|
|
1087
|
+
// startup sweep 由来の restart には待受者がいない。孤児 result を残さない。
|
|
1088
|
+
if (!isDelegateEvent && !(isRestart && startup)) await this._writeResult(request_id, result)
|
|
937
1089
|
// 要求ファイルは処理後に必ず削除する (成否問わず、再処理させないため)。
|
|
938
1090
|
try {
|
|
939
1091
|
await unlink(fp)
|
|
@@ -989,6 +1141,8 @@ export class SiblingInjectBridge {
|
|
|
989
1141
|
/* ignore */
|
|
990
1142
|
}
|
|
991
1143
|
this._watcher = null
|
|
1144
|
+
if (this._sweepTimer) clearTimeout(this._sweepTimer)
|
|
1145
|
+
this._sweepTimer = null
|
|
992
1146
|
}
|
|
993
1147
|
}
|
|
994
1148
|
|