Skip to content

feat: 非同期送信モード(send_mode: async)を追加 - #7

Open
y-oga-819 wants to merge 10 commits into
satetsu888:mainfrom
y-oga-819:feat/async-send-mode-upstream
Open

feat: 非同期送信モード(send_mode: async)を追加#7
y-oga-819 wants to merge 10 commits into
satetsu888:mainfrom
y-oga-819:feat/async-send-mode-upstream

Conversation

@y-oga-819

Copy link
Copy Markdown

概要

opt-in の非同期送信モード(send_mode: async)を追加します。async では hook 経由の send が detached worker を spawn して即 return し、HTTPS 送信を Claude Code の critical path から外します。worker は per-session ロックで同一セッションの送信を直列化し(サーバ負荷 ≤ sync)、ロックは pid / 経過時間で stale 自己回復します。デフォルトは sync のままで既存挙動は不変です。

使い方

  • 新規: agentrace init --url <url> --async
  • 既存環境: agentrace on --async
  • 確認: agentrace doctorSend mode
  • 手動送信(--claude-session-id)は常に同期
フロー(async)
graph LR
  CC[Claude Code hook] -->|同期・高速| S["npx agentrace send"]
  S --> CFG{config.send_mode}
  CFG -->|sync| SYNC["従来どおり同期送信"]
  CFG -->|async| SP["spawn detached worker → exit 0"]
  SP -.->|待たない| CC
  SP ==> W["__send-worker (短命・per-fire)"]
  W --> LK["per-session lock 取得"]
  LK --> ST["sendTranscript: getNewLines→sendIngest→saveCursor"]
  ST --> SV[(AgenTrace Server)]
Loading
正常系シーケンス(同一セッションに発火が連続したとき)

保持1+待機1 の上限で worker の積み上がりを防ぎ、drop された分は待機者の cursor→末尾読込が回収する。

sequenceDiagram
  participant CC as Claude Code
  participant P as send (親/hook)
  participant W1 as worker A1
  participant W2 as worker A2
  participant LK as lock(A)
  participant SV as Server
  CC->>P: 発火#1
  P-)W1: spawn detached + exit0
  W1->>LK: mkdir 成功(保持)
  W1->>SV: sendIngest [cursor..末尾]
  CC->>P: 発火#2
  P-)W2: spawn detached + exit0
  W2->>LK: mkdir 失敗 → 待機枠を確保して待つ
  CC->>P: 発火#3
  Note over P: 保持1+待機1 が埋まっている → worker は即 exit(drop)
  SV-->>W1: 200 OK → saveCursor
  W1->>LK: rmdir(解放)
  W2->>LK: mkdir 成功
  W2->>SV: sendIngest [新cursor..末尾]  ← #2,#3 の行も含む
  W2->>LK: rmdir
Loading

検証

  • cd cli && npm test → 76 passed
  • npx tsc --noEmit → clean
  • 手動 e2e: async は即 return + 別プロセスが届ける / sync は遅延サーバ相手でも送信完了まで待つ(後方互換)

y-oga-819 and others added 10 commits June 15, 2026 15:55
非同期送信モード(send_mode: async)の土台として、config に
send_mode フィールドと getSendMode() を追加する。未設定・不正値は
"sync" にフォールバックし、既存ユーザーの同期挙動を後方互換で維持する(HC-1)。

sendIngest の fetch に AbortSignal.timeout(SEND_TIMEOUT_MS≈30s) を付与し、
ハングした送信が後続 Phase の per-session ロックを長時間保持するのを防ぐ
(HC-6 の前提)。MAX_LOCK_MS(60s) > SEND_TIMEOUT_MS の不等式の土台となる。
タイムアウト/Abort は既存の catch で {ok:false} に正規化される。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
既存実装(cursor.ts 等)のコメント密度に合わせ、設計書を読まないと
わからない情報やコードから自明な説明をコメントから外す。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
非同期送信で同一セッションの送信を直列化するため、ディレクトリ存在を
ロック実体とする per-session ロックを実装する。保持1+待機1 の上限で
worker の積み上がりを防ぎ、超過分は待機者の cursor→末尾読込が回収する。

ロックは pid 死亡(即時)または経過時間(MAX_LOCK_MS=60s)で stale と
判定して自己回復する。MAX_LOCK_MS は送信タイムアウト(30s)より大きく取り、
送信中の生存 holder を誤って奪わない。

stale 奪取は取得ごとに一意な instanceId を持たせ、退避直前に同一インスタンス
かを再検証することで、別 worker が再取得した新 holder を誤って奪う二重保持を
防ぐ。再検証と rename の間に残る極小窓で万一二重保持が起きても、欠落は
カーソル+サーバ冪等が防ぐ(送信2本に留まりデッドロックしない)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
非同期送信の本体を追加する。send.ts の送信ロジックを process.exit に
依存しない純送信部 runSend に分離し、sync(hook)/manual は wrapper が
従来どおりの exit code とログを担う(後方互換を維持)。

worker はロック取得後に read→送信→saveCursor を行い、解放は saveCursor
後の finally で実施する(read はロック取得後・解放は cursor 前進後、の
順序不変条件を担保)。ロックが取れなければ送信せず終了し、待機者の
cursor→末尾読込が取りこぼし分を回収する。

ロック解放は自分が保持している場合のみ削除するよう変更した。送信が長時間
ハングして別プロセスにロックを奪取・再取得された後でも、新しい保持者の
ロックを誤って削除しない。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
send_mode が async のとき、hook 経由の send は payload を env に載せた
detached worker(隠し __send-worker サブコマンド)を spawn して即 exit する。
HTTPS 送信を hook の critical path から外し、Claude Code の応答性を
外部サーバのレイテンシから切り離す。

worker 起動は process.execPath + process.execArgv + process.argv[1] で
構成し、dev(tsx loader を execArgv が保持)/ 本番(dist の素の node)を
分岐なく解決する。env キーは WORKER_ENV 定数で spawn 側と read 側を共有する。

async では UserPromptSubmit の 10 秒待機を行わない(未書き込み分は次の
発火がカーソルから拾う)。sync・手動送信は従来どおり同期送信のまま。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
async 送信で spawn が失敗(同期 throw / 非同期 error)しても hook が
クラッシュしないよう、try/catch で sync 送信へフォールバックし、子プロセスに
error ハンドラを付ける。fork 資源枯渇などの異常時でも hook 契約(落とさない・
バッチを捨てない)を守る。

並行 stale 奪取テストは固定 sleep で待機者の生存を仮定していたため、高負荷で
spawn 間隔が広がると勝者が先に exit して別プロセスが stale 再取得し稀に複数
勝者になっていた。sentinel ファイルで全 racer の取得試行が出揃うまで全員を
生存させ、holder 一意性を atomic mkdir のみに依存させて決定論化した。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
init / on に --async を追加し send_mode を config に保存、doctor で現在の
send_mode を表示する。送信モードの切替を CLI から行えるようにする。

send_mode の保存は persistSendMode で実効 config(tree 内の local を優先、
無ければ global)に書き込む。これは送信時に loadConfigWithFallback が読む
ファイルと一致するため、project-local 設定のみの環境でも切替が確実に効く。

cli/CLAUDE.md に send_mode(sync/async)と --async 導線の説明を追記。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
spawn() の launch 失敗は throw ではなく非同期の 'error' イベントで
報告されるため、try/catch では捕捉されず「同期フォールバックする」
というコメントは誤り。実際は cursor が HTTP 200 でのみ進むため、
worker が起動失敗しても次回発火でリトライされ無害である旨に修正。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
初回送信時の `git remote get-url` / `git branch --show-current` は
timeout なしで実行しており、git がハング(stuck .git/index.lock 等)
すると holder ロックを MAX_LOCK_MS 超で保持し、後続発火が stale 判定で
二重 holder 化する恐れがあった。5s の timeout を付与。超過時は既存の
catch→null に流れ、git 情報なし(非 git リポジトリと同等)として送信する。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
並行ロックテストが生成する __lock_racer__.ts を cli/src/send/ 配下に
書き込んでいた(gitignore 対象外で、SIGKILL 時に残留・read-only
チェックアウトで失敗しうる)。スイート他箇所と同様 tmpHome に出力し、
lock.ts を絶対 file URL で import するよう変更。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@y-oga-819
y-oga-819 marked this pull request as ready for review June 15, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant