@cocorograph/hub-agent 0.8.30 → 0.8.32

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.30",
3
+ "version": "0.8.32",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -76,9 +76,22 @@ export const DELEGATION_LEDGER_RETENTION_MS = 24 * 60 * 60 * 1000
76
76
 
77
77
  const execFile = promisify(execFileCallback)
78
78
 
79
- async function defaultPidStartedAt(pid) {
80
- const { stdout } = await execFile("ps", ["-o", "lstart=", "-p", String(pid)])
81
- return stdout.replace(/\r?\n$/, "")
79
+ /**
80
+ * `ps` の起動時刻を送受信双方で比較する契約形へ正規化する。
81
+ * 先頭・末尾の Unicode 空白だけを除去し、日時本体の内部空白は変更しない。
82
+ * 正本は Hub の `codex_agent.py:_normalize_process_started_at`。
83
+ */
84
+ export function normalizeProcessStartedAt(raw) {
85
+ return raw.trim()
86
+ }
87
+
88
+ export async function defaultPidStartedAt(pid, exec = execFile) {
89
+ // 送信側と同じ TZ/locale で比較可能な文字列を作る。契約の正本は Hub の codex_agent.py。
90
+ // launchd / WSL の環境は利用者のシェルと一致しないため、常駐サービスの環境は継承しない。
91
+ const { stdout } = await exec("ps", ["-o", "lstart=", "-p", String(pid)], {
92
+ env: { TZ: "UTC", LC_ALL: "C", LANG: "C" },
93
+ })
94
+ return normalizeProcessStartedAt(stdout)
82
95
  }
83
96
 
84
97
  const DELIVERY_UNCONFIRMED_HINT =
@@ -343,6 +356,13 @@ export class SiblingInjectBridge {
343
356
  }
344
357
  }
345
358
 
359
+ _acceptsDelegateEvent(cwd, sessionId, event) {
360
+ this._pruneDelegations()
361
+ if (event.phase !== "started") return true
362
+ const sessionKey = this._delegationSessionKey(cwd, sessionId)
363
+ return !this._terminalDelegations.get(sessionKey)?.has(event.delegation_id)
364
+ }
365
+
346
366
  async _delegateProcessAlive(event) {
347
367
  if (!Number.isInteger(event.pid) || event.pid <= 0 || !event.pid_started_at) return true
348
368
  try {
@@ -352,7 +372,10 @@ export class SiblingInjectBridge {
352
372
  if (err?.code !== "EPERM") return true
353
373
  }
354
374
  try {
355
- return (await this._pidStartedAt(event.pid)) === event.pid_started_at
375
+ return (
376
+ normalizeProcessStartedAt(await this._pidStartedAt(event.pid)) ===
377
+ normalizeProcessStartedAt(event.pid_started_at)
378
+ )
356
379
  } catch {
357
380
  return true
358
381
  }
@@ -598,17 +621,23 @@ export class SiblingInjectBridge {
598
621
  ...(pid_started_at !== undefined ? { pid_started_at } : {}),
599
622
  timestamp: timestamp || new Date().toISOString(),
600
623
  }
601
- const r = this.claudeBridge?.emitToStream
602
- ? this.claudeBridge.emitToStream(cwd, target_session_id, event)
603
- : { status: "not_found" }
604
- if (r.status === "delivered") {
605
- this._recordDelegateEvent(cwd, target_session_id, event)
606
- result = { ok: true, delivered_to: r.session }
624
+ // 墓標確認から配送成功時の台帳反映まで await を挟まない同期区間に閉じる。
625
+ // 配送失敗した started は台帳へ残さず、終端後に遅着した started は配送前に拒否する。
626
+ if (!this._acceptsDelegateEvent(cwd, target_session_id, event)) {
627
+ result = { ok: false, error: "stale_delegate_event" }
607
628
  } else {
608
- result = {
609
- ok: false,
610
- error: AMBIGUOUS_TARGET_ERROR,
611
- ...(r.candidates ? { candidates: r.candidates } : {}),
629
+ const r = this.claudeBridge?.emitToStream
630
+ ? this.claudeBridge.emitToStream(cwd, target_session_id, event)
631
+ : { status: "not_found" }
632
+ if (r.status === "delivered") {
633
+ this._recordDelegateEvent(cwd, target_session_id, event)
634
+ result = { ok: true, delivered_to: r.session }
635
+ } else {
636
+ result = {
637
+ ok: false,
638
+ error: AMBIGUOUS_TARGET_ERROR,
639
+ ...(r.candidates ? { candidates: r.candidates } : {}),
640
+ }
612
641
  }
613
642
  }
614
643
  }