coord/session.py

173 lines
5.9 KiB
Python
Raw Permalink Normal View History

#!/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)