172 lines
5.9 KiB
Python
Executable file
172 lines
5.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Best-effort session lifecycle bridge for Claude Code and Codex hooks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import importlib.machinery
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import socket
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
SPEC = importlib.util.spec_from_loader(
|
|
"coord_cli",
|
|
importlib.machinery.SourceFileLoader("coord_cli", str(HERE / "coord")),
|
|
)
|
|
cli = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
SPEC.loader.exec_module(cli)
|
|
|
|
|
|
def annotate_tmux(runtime_id: str, conversation_id: str) -> None:
|
|
pane = os.environ.get("TMUX_PANE")
|
|
if not pane:
|
|
return
|
|
for key, value in (
|
|
("@coord_instance", runtime_id),
|
|
("@coord_conversation", conversation_id),
|
|
("@coord_task", os.environ.get("FLEET_AI_TASK", "")),
|
|
):
|
|
subprocess.run(
|
|
["tmux", "set-option", "-p", "-q", "-t", pane, key, value],
|
|
check=False,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
|
|
def subagent_identity(conversation_id: str, agent_id: str) -> tuple[str, str]:
|
|
"""Derive the stable conversation/runtime pair shared by all hook processes."""
|
|
conversation = f"{conversation_id}:agent:{agent_id}"
|
|
runtime = str(uuid.uuid5(uuid.NAMESPACE_URL, f"fleet-agent-coord:{conversation}"))
|
|
return conversation, runtime
|
|
|
|
|
|
def main() -> None:
|
|
event = json.loads(sys.stdin.read())
|
|
conversation = event.get("session_id") or ""
|
|
if not conversation:
|
|
return
|
|
agent_id = str(event.get("agent_id") or "")
|
|
db = cli.store()
|
|
name = event.get("hook_event_name") or ""
|
|
requested = os.environ.get("COORD_INSTANCE_ID", "")
|
|
if agent_id:
|
|
conversation, requested = subagent_identity(conversation, agent_id)
|
|
harness = (
|
|
"codex"
|
|
if os.environ.get("CODEX_THREAD_ID")
|
|
else "claude-code"
|
|
)
|
|
|
|
if name == "SessionEnd":
|
|
# Without a launcher-provided runtime UUID, conversation identity is
|
|
# insufficient: a stale process could otherwise end a newer resume.
|
|
if not requested:
|
|
return
|
|
try:
|
|
db.end_instance(db.resolve_runtime(requested), reason="clean")
|
|
except KeyError:
|
|
pass
|
|
return
|
|
|
|
try:
|
|
# A direct, unwrapped harness cannot communicate a newly generated UUID
|
|
# to later hook processes. Reuse its one live conversation attachment;
|
|
# managed launchers always provide a fresh exact runtime.
|
|
if not requested:
|
|
runtime_id = db.resolve_runtime(conversation)
|
|
db.heartbeat(
|
|
runtime_id,
|
|
renew_lease_ttl_seconds=cli.storelib.DEFAULT_LEASE_TTL,
|
|
)
|
|
else:
|
|
runtime_id = db.register_instance(
|
|
conversation,
|
|
runtime_id=requested,
|
|
harness=harness,
|
|
host=socket.gethostname(),
|
|
cwd=str(pathlib.Path(event.get("cwd") or os.getcwd()).resolve()),
|
|
label=os.environ.get("FLEET_AI_TASK", ""),
|
|
)
|
|
except (KeyError, sqlite3.IntegrityError):
|
|
if not requested:
|
|
runtime_id = db.register_instance(
|
|
conversation,
|
|
harness=harness,
|
|
host=socket.gethostname(),
|
|
cwd=str(pathlib.Path(event.get("cwd") or os.getcwd()).resolve()),
|
|
label=os.environ.get("FLEET_AI_TASK", ""),
|
|
)
|
|
else:
|
|
runtime_id = db.resolve_runtime(requested)
|
|
db.heartbeat(
|
|
runtime_id,
|
|
renew_lease_ttl_seconds=cli.storelib.DEFAULT_LEASE_TTL,
|
|
)
|
|
annotate_tmux(runtime_id, conversation)
|
|
|
|
peers = [item for item in db.live_instances() if item["runtime_id"] != runtime_id]
|
|
leases = [item for item in db.active_leases() if item["runtime_id"] != runtime_id]
|
|
pending = db.inbox(runtime_id, limit=5, mark_delivered=True)
|
|
if not peers and not pending:
|
|
return
|
|
lines: list[str] = []
|
|
if peers:
|
|
lines.append(f"Live peer agents: {len(peers)}")
|
|
for peer in peers:
|
|
lines.append(
|
|
f" · {cli.runtime_handle(peer['runtime_id'])} "
|
|
f"{peer['harness']} task={cli.peer_label(peer)}"
|
|
)
|
|
if leases:
|
|
lines.append("Active peer resource leases:")
|
|
for lease in leases:
|
|
lines.append(
|
|
f" · {lease['path']} [{lease['mode']}] by "
|
|
f"{cli.runtime_handle(lease['runtime_id'])} — {lease['purpose']}"
|
|
)
|
|
if pending:
|
|
lines.append("Pending durable peer messages:")
|
|
for message in pending:
|
|
sender = cli.runtime_handle(message.sender_runtime_id)
|
|
if message.state == "delivered":
|
|
digest = hashlib.sha256(message.body.encode("utf-8")).hexdigest()[:10]
|
|
lines.append(
|
|
f" · [{message.message_id}] {sender} pending again "
|
|
f"({message.kind}, sha256:{digest}); `coord inbox --peek`"
|
|
)
|
|
else:
|
|
lines.append(
|
|
f" · [{message.message_id}] {sender} "
|
|
f"({message.kind}): {message.body}"
|
|
)
|
|
lines.append(
|
|
"Use an isolated managed clone for writable work; claim shared integration, "
|
|
"push, activation, and deployment resources before those operations."
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "SessionStart",
|
|
"additionalContext": "\n".join(lines),
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as exc: # lifecycle hooks must fail open
|
|
print(f"coord-session: failing open: {exc}", file=sys.stderr)
|
|
raise SystemExit(0)
|