pairling 0.2.10 → 0.2.11
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 +3 -3
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/pairlingd.py +426 -103
- package/payload/mac/connectd/internal/gateway/proxy.go +1 -0
- package/payload/mac/install/install-runtime.sh +5 -7
- package/payload-manifest.json +9 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pairling",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Pair your iPhone with the AI coding agents running on your Mac. CLI and local runtime installer for the Pairling iOS app.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pairling",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"url": "https://github.com/mergimg0/pairling-helper"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@pairling/runtime-darwin-arm64": "0.2.
|
|
44
|
-
"@pairling/runtime-darwin-x64": "0.2.
|
|
43
|
+
"@pairling/runtime-darwin-arm64": "0.2.11",
|
|
44
|
+
"@pairling/runtime-darwin-x64": "0.2.11"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
83c69e1
|
package/payload/mac/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.11
|
|
@@ -456,10 +456,22 @@ AUTH_RESULT_CACHE_SECONDS = max(0.0, float(os.environ.get("PAIRLING_AUTH_RESULT_
|
|
|
456
456
|
AUTH_RESULT_CACHE_MAX = max(16, int(os.environ.get("PAIRLING_AUTH_RESULT_CACHE_MAX", "512")))
|
|
457
457
|
RUNTIME_MAX_ACTIVE_FAST_REQUESTS = max(2, int(os.environ.get("PAIRLING_RUNTIME_MAX_ACTIVE_FAST_REQUESTS", "4")))
|
|
458
458
|
RUNTIME_MAX_ACTIVE_REQUESTS = max(4, int(os.environ.get("PAIRLING_RUNTIME_MAX_ACTIVE_REQUESTS", "12")))
|
|
459
|
-
|
|
459
|
+
RUNTIME_MAX_ACTIVE_DASHBOARD_STREAMS = max(2, int(os.environ.get("PAIRLING_RUNTIME_MAX_ACTIVE_DASHBOARD_STREAMS", "8")))
|
|
460
|
+
RUNTIME_MAX_ACTIVE_STREAMS = max(2, int(os.environ.get(
|
|
461
|
+
"PAIRLING_RUNTIME_MAX_ACTIVE_DETAIL_STREAMS",
|
|
462
|
+
os.environ.get("PAIRLING_RUNTIME_MAX_ACTIVE_STREAMS", "6"),
|
|
463
|
+
)))
|
|
464
|
+
RUNTIME_MAX_ACTIVE_AUX_STREAMS = max(2, int(os.environ.get("PAIRLING_RUNTIME_MAX_ACTIVE_AUX_STREAMS", "4")))
|
|
460
465
|
RUNTIME_MAX_ACTIVE_CONNECTIONS = max(
|
|
461
|
-
|
|
462
|
-
|
|
466
|
+
(
|
|
467
|
+
RUNTIME_MAX_ACTIVE_FAST_REQUESTS
|
|
468
|
+
+ RUNTIME_MAX_ACTIVE_REQUESTS
|
|
469
|
+
+ RUNTIME_MAX_ACTIVE_DASHBOARD_STREAMS
|
|
470
|
+
+ RUNTIME_MAX_ACTIVE_STREAMS
|
|
471
|
+
+ RUNTIME_MAX_ACTIVE_AUX_STREAMS
|
|
472
|
+
+ 2
|
|
473
|
+
),
|
|
474
|
+
int(os.environ.get("PAIRLING_RUNTIME_MAX_ACTIVE_CONNECTIONS", "28")),
|
|
463
475
|
)
|
|
464
476
|
TERMINAL_TRUTH_OSASCRIPT_TIMEOUT_SECONDS = max(
|
|
465
477
|
0.5,
|
|
@@ -564,7 +576,9 @@ _auth_result_cache_lock = threading.Lock()
|
|
|
564
576
|
_auth_result_cache: dict[tuple, tuple[float, object]] = {}
|
|
565
577
|
_FAST_ADMISSION_SEMAPHORE = threading.BoundedSemaphore(RUNTIME_MAX_ACTIVE_FAST_REQUESTS)
|
|
566
578
|
_REQUEST_ADMISSION_SEMAPHORE = threading.BoundedSemaphore(RUNTIME_MAX_ACTIVE_REQUESTS)
|
|
579
|
+
_DASHBOARD_STREAM_ADMISSION_SEMAPHORE = threading.BoundedSemaphore(RUNTIME_MAX_ACTIVE_DASHBOARD_STREAMS)
|
|
567
580
|
_STREAM_ADMISSION_SEMAPHORE = threading.BoundedSemaphore(RUNTIME_MAX_ACTIVE_STREAMS)
|
|
581
|
+
_AUX_STREAM_ADMISSION_SEMAPHORE = threading.BoundedSemaphore(RUNTIME_MAX_ACTIVE_AUX_STREAMS)
|
|
568
582
|
_CONNECTION_ADMISSION_SEMAPHORE = threading.BoundedSemaphore(RUNTIME_MAX_ACTIVE_CONNECTIONS)
|
|
569
583
|
_STREAM_ENDPOINTS = {
|
|
570
584
|
"/health-stream",
|
|
@@ -582,6 +596,16 @@ _STREAM_ENDPOINTS = {
|
|
|
582
596
|
"/turn-state-stream",
|
|
583
597
|
"/llm-route-stream",
|
|
584
598
|
}
|
|
599
|
+
_DASHBOARD_STREAM_ENDPOINTS = {
|
|
600
|
+
"/health-stream",
|
|
601
|
+
"/sessions-stream",
|
|
602
|
+
}
|
|
603
|
+
_AUX_STREAM_ENDPOINTS = {
|
|
604
|
+
"/activity-stream",
|
|
605
|
+
"/commands-stream",
|
|
606
|
+
"/invocations-stream",
|
|
607
|
+
"/llm-route-stream",
|
|
608
|
+
}
|
|
585
609
|
_FAST_ENDPOINTS = {"/health", "/healthz", "/readyz", "/routez", "/power-state", "/manifest"}
|
|
586
610
|
|
|
587
611
|
|
|
@@ -1975,6 +1999,14 @@ def _runtime_admission_for_path(path: str) -> _RuntimeAdmission:
|
|
|
1975
1999
|
if _FAST_ADMISSION_SEMAPHORE.acquire(blocking=False):
|
|
1976
2000
|
return _RuntimeAdmission(_FAST_ADMISSION_SEMAPHORE, True)
|
|
1977
2001
|
return _RuntimeAdmission(None, False, "fast_capacity_exceeded")
|
|
2002
|
+
if path in _DASHBOARD_STREAM_ENDPOINTS:
|
|
2003
|
+
if _DASHBOARD_STREAM_ADMISSION_SEMAPHORE.acquire(blocking=False):
|
|
2004
|
+
return _RuntimeAdmission(_DASHBOARD_STREAM_ADMISSION_SEMAPHORE, True)
|
|
2005
|
+
return _RuntimeAdmission(None, False, "dashboard_stream_capacity_exceeded")
|
|
2006
|
+
if path in _AUX_STREAM_ENDPOINTS:
|
|
2007
|
+
if _AUX_STREAM_ADMISSION_SEMAPHORE.acquire(blocking=False):
|
|
2008
|
+
return _RuntimeAdmission(_AUX_STREAM_ADMISSION_SEMAPHORE, True)
|
|
2009
|
+
return _RuntimeAdmission(None, False, "aux_stream_capacity_exceeded")
|
|
1978
2010
|
if path in _STREAM_ENDPOINTS:
|
|
1979
2011
|
if _STREAM_ADMISSION_SEMAPHORE.acquire(blocking=False):
|
|
1980
2012
|
return _RuntimeAdmission(_STREAM_ADMISSION_SEMAPHORE, True)
|
|
@@ -2800,6 +2832,7 @@ def _pid_for_tty_command(tty: str, command_name: str) -> int:
|
|
|
2800
2832
|
|
|
2801
2833
|
|
|
2802
2834
|
_codex_terminal_scan_cache: dict[str, object] = {"ts": 0.0, "rows": []}
|
|
2835
|
+
_claude_terminal_scan_cache: dict[str, object] = {"ts": 0.0, "rows": []}
|
|
2803
2836
|
_codex_task_boundary_cache: dict[str, dict[str, object]] = {}
|
|
2804
2837
|
|
|
2805
2838
|
|
|
@@ -2831,6 +2864,11 @@ def _is_codex_cli_command(command: str) -> bool:
|
|
|
2831
2864
|
)
|
|
2832
2865
|
|
|
2833
2866
|
|
|
2867
|
+
def _is_claude_cli_command(command: str) -> bool:
|
|
2868
|
+
lower = (command or "").lower()
|
|
2869
|
+
return re.search(r"(^|/)claude(\s|$)", lower) is not None
|
|
2870
|
+
|
|
2871
|
+
|
|
2834
2872
|
def _codex_live_terminal_rows() -> list[dict]:
|
|
2835
2873
|
now = _time.time()
|
|
2836
2874
|
cached_ts = float(_codex_terminal_scan_cache.get("ts") or 0)
|
|
@@ -2883,6 +2921,56 @@ def _codex_live_terminal_rows() -> list[dict]:
|
|
|
2883
2921
|
return rows
|
|
2884
2922
|
|
|
2885
2923
|
|
|
2924
|
+
def _claude_live_terminal_rows() -> list[dict]:
|
|
2925
|
+
now = _time.time()
|
|
2926
|
+
cached_ts = float(_claude_terminal_scan_cache.get("ts") or 0)
|
|
2927
|
+
if now - cached_ts < 2:
|
|
2928
|
+
return list(_claude_terminal_scan_cache.get("rows") or [])
|
|
2929
|
+
try:
|
|
2930
|
+
proc = subprocess.run(
|
|
2931
|
+
["ps", "-axo", "pid=,tty=,lstart=,command="],
|
|
2932
|
+
capture_output=True,
|
|
2933
|
+
text=True,
|
|
2934
|
+
timeout=2,
|
|
2935
|
+
)
|
|
2936
|
+
except Exception:
|
|
2937
|
+
return []
|
|
2938
|
+
if proc.returncode != 0:
|
|
2939
|
+
return []
|
|
2940
|
+
|
|
2941
|
+
by_tty: dict[str, dict] = {}
|
|
2942
|
+
for line in proc.stdout.splitlines():
|
|
2943
|
+
parts = line.strip().split(None, 7)
|
|
2944
|
+
if len(parts) < 8 or not parts[0].isdigit():
|
|
2945
|
+
continue
|
|
2946
|
+
tty_name = parts[1]
|
|
2947
|
+
command = parts[7]
|
|
2948
|
+
if tty_name == "??" or not _is_claude_cli_command(command):
|
|
2949
|
+
continue
|
|
2950
|
+
try:
|
|
2951
|
+
started = datetime.strptime(" ".join(parts[2:7]), "%a %b %d %H:%M:%S %Y").timestamp()
|
|
2952
|
+
except Exception:
|
|
2953
|
+
started = 0.0
|
|
2954
|
+
pid = int(parts[0])
|
|
2955
|
+
tty = f"/dev/{tty_name}"
|
|
2956
|
+
cwd = _process_cwd(pid)
|
|
2957
|
+
if not cwd:
|
|
2958
|
+
continue
|
|
2959
|
+
current = by_tty.get(tty)
|
|
2960
|
+
if current is None or pid < int(current.get("pid") or 0):
|
|
2961
|
+
by_tty[tty] = {
|
|
2962
|
+
"pid": pid,
|
|
2963
|
+
"tty": tty,
|
|
2964
|
+
"project": cwd,
|
|
2965
|
+
"started_at": started,
|
|
2966
|
+
"command": command,
|
|
2967
|
+
}
|
|
2968
|
+
rows = list(by_tty.values())
|
|
2969
|
+
_claude_terminal_scan_cache["ts"] = now
|
|
2970
|
+
_claude_terminal_scan_cache["rows"] = rows
|
|
2971
|
+
return rows
|
|
2972
|
+
|
|
2973
|
+
|
|
2886
2974
|
def _codex_discover_terminal_control(project: str, observed_started_at: float) -> dict | None:
|
|
2887
2975
|
if not project or observed_started_at <= 0:
|
|
2888
2976
|
return None
|
|
@@ -2898,6 +2986,109 @@ def _codex_discover_terminal_control(project: str, observed_started_at: float) -
|
|
|
2898
2986
|
return best if delta <= 600 else None
|
|
2899
2987
|
|
|
2900
2988
|
|
|
2989
|
+
def _codex_terminal_native_id(row: dict) -> str:
|
|
2990
|
+
tty = str(row.get("tty") or "")
|
|
2991
|
+
project = str(row.get("project") or "")
|
|
2992
|
+
started = int(float(row.get("started_at") or 0))
|
|
2993
|
+
seed = f"{tty}|{project}|{started}"
|
|
2994
|
+
return "terminal-" + hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
|
|
2995
|
+
|
|
2996
|
+
|
|
2997
|
+
def _claude_terminal_native_id(row: dict) -> str:
|
|
2998
|
+
tty = str(row.get("tty") or "")
|
|
2999
|
+
project = str(row.get("project") or "")
|
|
3000
|
+
started = int(float(row.get("started_at") or 0))
|
|
3001
|
+
seed = f"{tty}|{project}|{started}"
|
|
3002
|
+
return "terminal-" + hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
|
|
3003
|
+
|
|
3004
|
+
|
|
3005
|
+
def _codex_register_terminal_only_rows(seen: set[str]) -> None:
|
|
3006
|
+
for terminal in _codex_live_terminal_rows():
|
|
3007
|
+
tty = str(terminal.get("tty") or "")
|
|
3008
|
+
project = str(terminal.get("project") or "")
|
|
3009
|
+
pid = int(terminal.get("pid") or 0)
|
|
3010
|
+
if not project or not pid or not re.match(r"^/dev/ttys[0-9]{3,}$", tty):
|
|
3011
|
+
continue
|
|
3012
|
+
existing = _agent_registry_get_by_tty("codex", tty)
|
|
3013
|
+
if existing and not existing.get("closed_at"):
|
|
3014
|
+
native_id = existing.get("native_id") or ""
|
|
3015
|
+
if native_id in seen:
|
|
3016
|
+
continue
|
|
3017
|
+
_agent_registry_update_control(
|
|
3018
|
+
"codex",
|
|
3019
|
+
native_id,
|
|
3020
|
+
pid=pid,
|
|
3021
|
+
terminal_tty=tty,
|
|
3022
|
+
state="running",
|
|
3023
|
+
reopen=True,
|
|
3024
|
+
)
|
|
3025
|
+
continue
|
|
3026
|
+
native_id = _codex_terminal_native_id(terminal)
|
|
3027
|
+
if native_id in seen:
|
|
3028
|
+
continue
|
|
3029
|
+
_agent_registry_upsert(
|
|
3030
|
+
"codex",
|
|
3031
|
+
native_id,
|
|
3032
|
+
project,
|
|
3033
|
+
pid=pid,
|
|
3034
|
+
terminal_tty=tty,
|
|
3035
|
+
state="running",
|
|
3036
|
+
metadata={
|
|
3037
|
+
"discovered_by": "terminal_scan",
|
|
3038
|
+
"terminal_only": True,
|
|
3039
|
+
"command": str(terminal.get("command") or "")[:500],
|
|
3040
|
+
},
|
|
3041
|
+
working_on="Live Codex terminal",
|
|
3042
|
+
)
|
|
3043
|
+
|
|
3044
|
+
|
|
3045
|
+
def _registry_row_has_live_terminal(row: dict, command_name: str) -> bool:
|
|
3046
|
+
tty = str(row.get("terminal_tty") or "")
|
|
3047
|
+
if not re.match(r"^/dev/ttys[0-9]{3,}$", tty):
|
|
3048
|
+
return False
|
|
3049
|
+
return bool(_pid_for_tty_command(tty, command_name))
|
|
3050
|
+
|
|
3051
|
+
|
|
3052
|
+
def _claude_register_terminal_only_rows(seen: set[str]) -> None:
|
|
3053
|
+
for terminal in _claude_live_terminal_rows():
|
|
3054
|
+
tty = str(terminal.get("tty") or "")
|
|
3055
|
+
project = str(terminal.get("project") or "")
|
|
3056
|
+
pid = int(terminal.get("pid") or 0)
|
|
3057
|
+
if not project or not pid or not re.match(r"^/dev/ttys[0-9]{3,}$", tty):
|
|
3058
|
+
continue
|
|
3059
|
+
existing = _agent_registry_get_by_tty("claude", tty)
|
|
3060
|
+
if existing and not existing.get("closed_at"):
|
|
3061
|
+
native_id = existing.get("native_id") or ""
|
|
3062
|
+
if native_id in seen:
|
|
3063
|
+
continue
|
|
3064
|
+
_agent_registry_update_control(
|
|
3065
|
+
"claude",
|
|
3066
|
+
native_id,
|
|
3067
|
+
pid=pid,
|
|
3068
|
+
terminal_tty=tty,
|
|
3069
|
+
state="running",
|
|
3070
|
+
reopen=True,
|
|
3071
|
+
)
|
|
3072
|
+
continue
|
|
3073
|
+
native_id = _claude_terminal_native_id(terminal)
|
|
3074
|
+
if native_id in seen:
|
|
3075
|
+
continue
|
|
3076
|
+
_agent_registry_upsert(
|
|
3077
|
+
"claude",
|
|
3078
|
+
native_id,
|
|
3079
|
+
project,
|
|
3080
|
+
pid=pid,
|
|
3081
|
+
terminal_tty=tty,
|
|
3082
|
+
state="running",
|
|
3083
|
+
metadata={
|
|
3084
|
+
"discovered_by": "terminal_scan",
|
|
3085
|
+
"terminal_only": True,
|
|
3086
|
+
"command": str(terminal.get("command") or "")[:500],
|
|
3087
|
+
},
|
|
3088
|
+
working_on="Live Claude terminal",
|
|
3089
|
+
)
|
|
3090
|
+
|
|
3091
|
+
|
|
2901
3092
|
# Phase 4 B.3: warm claude --continue pool. Maintains up to one long-running
|
|
2902
3093
|
# `claude` session per model. After 5 min idle, the worker exits.
|
|
2903
3094
|
# This turns 18-25s cold start into ~2s for repeat /llm-route calls.
|
|
@@ -4388,8 +4579,10 @@ class ClaudeSessionsSqliteBackend:
|
|
|
4388
4579
|
if live_only:
|
|
4389
4580
|
source = [
|
|
4390
4581
|
row for row in _agent_registry_live("claude")
|
|
4391
|
-
if
|
|
4392
|
-
|
|
4582
|
+
if (
|
|
4583
|
+
row.get("claude_uuid")
|
|
4584
|
+
and float(row.get("last_heartbeat") or 0) > cutoff
|
|
4585
|
+
) or _registry_row_has_live_terminal(row, "claude")
|
|
4393
4586
|
]
|
|
4394
4587
|
else:
|
|
4395
4588
|
source = _agent_registry_recent("claude", since_min=within_min, limit=1000)
|
|
@@ -4415,7 +4608,7 @@ class ClaudeSessionsSqliteBackend:
|
|
|
4415
4608
|
def collect_rows(self, since_min: int, live_only: bool, limit: int) -> list[dict]:
|
|
4416
4609
|
rows: list[dict] = []
|
|
4417
4610
|
for row in _agent_registry_recent("claude", since_min=since_min, limit=1000):
|
|
4418
|
-
if not row.get("claude_uuid"):
|
|
4611
|
+
if not row.get("claude_uuid") and not _registry_row_has_live_terminal(row, "claude"):
|
|
4419
4612
|
continue
|
|
4420
4613
|
if live_only and row.get("closed_at") is not None:
|
|
4421
4614
|
continue
|
|
@@ -4908,17 +5101,7 @@ def _terminal_surface_source(raw_session: str) -> dict:
|
|
|
4908
5101
|
if provider not in AGENT_PROVIDERS:
|
|
4909
5102
|
return {"available": False, "source": "unavailable", "reason": "unsupported_provider"}
|
|
4910
5103
|
|
|
4911
|
-
|
|
4912
|
-
session = PTY_BROKER.get(qualified)
|
|
4913
|
-
if session is not None:
|
|
4914
|
-
return {
|
|
4915
|
-
"available": True,
|
|
4916
|
-
"source": "broker_vt",
|
|
4917
|
-
"reason": "broker_vt",
|
|
4918
|
-
"broker_id": _broker_session_id(session),
|
|
4919
|
-
"tty": _broker_slave_tty(session),
|
|
4920
|
-
"can_control": True,
|
|
4921
|
-
}
|
|
5104
|
+
broker_error = ""
|
|
4922
5105
|
|
|
4923
5106
|
if provider == "codex":
|
|
4924
5107
|
reg = _agent_registry_get("codex", native_id) or {}
|
|
@@ -4931,9 +5114,16 @@ def _terminal_surface_source(raw_session: str) -> dict:
|
|
|
4931
5114
|
metadata = {}
|
|
4932
5115
|
broker_id = str(metadata.get("broker_id") or "").strip()
|
|
4933
5116
|
if broker_id and PTY_BROKER is not None:
|
|
4934
|
-
|
|
5117
|
+
try:
|
|
5118
|
+
session = PTY_BROKER.get(broker_id)
|
|
5119
|
+
except Exception as e:
|
|
5120
|
+
session = None
|
|
5121
|
+
broker_error = str(e)[:160]
|
|
4935
5122
|
if session is not None:
|
|
4936
|
-
|
|
5123
|
+
try:
|
|
5124
|
+
PTY_BROKER.register_alias(qualified, _broker_session_id(session))
|
|
5125
|
+
except Exception:
|
|
5126
|
+
pass
|
|
4937
5127
|
return {
|
|
4938
5128
|
"available": True,
|
|
4939
5129
|
"source": "broker_vt",
|
|
@@ -4972,7 +5162,59 @@ def _terminal_surface_source(raw_session: str) -> dict:
|
|
|
4972
5162
|
"can_control": True,
|
|
4973
5163
|
}
|
|
4974
5164
|
|
|
4975
|
-
|
|
5165
|
+
if provider == "claude":
|
|
5166
|
+
reg = _agent_registry_get("claude", native_id) or {}
|
|
5167
|
+
metadata = {}
|
|
5168
|
+
try:
|
|
5169
|
+
metadata = json.loads(reg.get("metadata_json") or "{}")
|
|
5170
|
+
if not isinstance(metadata, dict):
|
|
5171
|
+
metadata = {}
|
|
5172
|
+
except Exception:
|
|
5173
|
+
metadata = {}
|
|
5174
|
+
broker_id = str(metadata.get("broker_id") or "").strip()
|
|
5175
|
+
if broker_id and PTY_BROKER is not None:
|
|
5176
|
+
try:
|
|
5177
|
+
session = PTY_BROKER.get(broker_id)
|
|
5178
|
+
except Exception as e:
|
|
5179
|
+
session = None
|
|
5180
|
+
broker_error = str(e)[:160]
|
|
5181
|
+
if session is not None:
|
|
5182
|
+
try:
|
|
5183
|
+
PTY_BROKER.register_alias(qualified, _broker_session_id(session))
|
|
5184
|
+
except Exception:
|
|
5185
|
+
pass
|
|
5186
|
+
return {
|
|
5187
|
+
"available": True,
|
|
5188
|
+
"source": "broker_vt",
|
|
5189
|
+
"reason": "broker_vt",
|
|
5190
|
+
"broker_id": _broker_session_id(session),
|
|
5191
|
+
"tty": _broker_slave_tty(session),
|
|
5192
|
+
"can_control": True,
|
|
5193
|
+
}
|
|
5194
|
+
tty = str(reg.get("terminal_tty") or "")
|
|
5195
|
+
if not tty:
|
|
5196
|
+
return {"available": False, "source": "unavailable", "reason": broker_error or "no_terminal_tty"}
|
|
5197
|
+
if not re.match(r"^/dev/ttys[0-9]{3,}$", tty):
|
|
5198
|
+
return {"available": False, "source": "unavailable", "reason": "invalid_tty", "tty": tty}
|
|
5199
|
+
capture_path = _terminal_capture_for_tty(tty, reg.get("project"))
|
|
5200
|
+
if capture_path and capture_path.exists():
|
|
5201
|
+
return {
|
|
5202
|
+
"available": True,
|
|
5203
|
+
"source": "script_capture",
|
|
5204
|
+
"reason": "script_capture",
|
|
5205
|
+
"terminal_log": str(capture_path),
|
|
5206
|
+
"tty": tty,
|
|
5207
|
+
"can_control": True,
|
|
5208
|
+
}
|
|
5209
|
+
return {
|
|
5210
|
+
"available": True,
|
|
5211
|
+
"source": "terminal_app_contents",
|
|
5212
|
+
"reason": "terminal_app_contents",
|
|
5213
|
+
"tty": tty,
|
|
5214
|
+
"can_control": True,
|
|
5215
|
+
}
|
|
5216
|
+
|
|
5217
|
+
return {"available": False, "source": "unavailable", "reason": broker_error or "no_terminal_tty"}
|
|
4976
5218
|
|
|
4977
5219
|
|
|
4978
5220
|
def _terminal_surface_capabilities(raw_session: str) -> dict:
|
|
@@ -5324,8 +5566,8 @@ def _session_runtime_truth_from_parts(
|
|
|
5324
5566
|
control_state = "blocked"
|
|
5325
5567
|
blocked_reason = contradictions[0]["code"] if contradictions else "terminal_surface_degraded"
|
|
5326
5568
|
elif selected_surface == "v1_fallback":
|
|
5327
|
-
control_state = "read_only"
|
|
5328
|
-
blocked_reason = "v1_fallback"
|
|
5569
|
+
control_state = "eligible" if selected.get("screen_hash") and selected.get("nonce") else "read_only"
|
|
5570
|
+
blocked_reason = None if control_state == "eligible" else "v1_fallback"
|
|
5329
5571
|
else:
|
|
5330
5572
|
control_state = "unavailable"
|
|
5331
5573
|
blocked_reason = "terminal_surface_unavailable"
|
|
@@ -6156,6 +6398,8 @@ def _decorate_claude_session_row(row: dict, native_id: str, claude_pid: int = 0,
|
|
|
6156
6398
|
row["provider"] = "claude"
|
|
6157
6399
|
row["native_id"] = native_id
|
|
6158
6400
|
row["id"] = _qualified_session_id("claude", native_id)
|
|
6401
|
+
row["terminal_tty"] = terminal_tty
|
|
6402
|
+
row["pid"] = claude_pid
|
|
6159
6403
|
capabilities = list(CLAUDE_SESSION_CAPABILITIES)
|
|
6160
6404
|
if terminal_tty and _terminal_capture_for_tty(terminal_tty, row.get("project")):
|
|
6161
6405
|
capabilities.append("terminal_output")
|
|
@@ -6179,6 +6423,35 @@ def _decorate_claude_session_row(row: dict, native_id: str, claude_pid: int = 0,
|
|
|
6179
6423
|
return row
|
|
6180
6424
|
|
|
6181
6425
|
|
|
6426
|
+
def _collapse_live_session_rows_by_terminal(rows: list[dict]) -> list[dict]:
|
|
6427
|
+
by_tty: dict[tuple[str, str], dict] = {}
|
|
6428
|
+
passthrough: list[dict] = []
|
|
6429
|
+
for row in rows:
|
|
6430
|
+
provider = str(row.get("provider") or "")
|
|
6431
|
+
tty = str(row.get("terminal_tty") or "")
|
|
6432
|
+
if row.get("closed_at") is not None or not provider or not re.match(r"^/dev/ttys[0-9]{3,}$", tty):
|
|
6433
|
+
passthrough.append(row)
|
|
6434
|
+
continue
|
|
6435
|
+
key = (provider, tty)
|
|
6436
|
+
current = by_tty.get(key)
|
|
6437
|
+
if current is None or _session_row_terminal_score(row) > _session_row_terminal_score(current):
|
|
6438
|
+
by_tty[key] = row
|
|
6439
|
+
return passthrough + list(by_tty.values())
|
|
6440
|
+
|
|
6441
|
+
|
|
6442
|
+
def _session_row_terminal_score(row: dict) -> tuple[int, int, int, int, int]:
|
|
6443
|
+
caps = set(row.get("capabilities") or [])
|
|
6444
|
+
control = row.get("controllability") if isinstance(row.get("controllability"), dict) else {}
|
|
6445
|
+
native_id = str(row.get("native_id") or row.get("id") or "")
|
|
6446
|
+
return (
|
|
6447
|
+
1 if bool(control.get("can_send_text") or control.get("can_terminate")) else 0,
|
|
6448
|
+
0 if native_id.startswith("terminal-") else 1,
|
|
6449
|
+
1 if row.get("claude_uuid") else 0,
|
|
6450
|
+
1 if "transcript" in caps else 0,
|
|
6451
|
+
int(row.get("last_heartbeat") or 0),
|
|
6452
|
+
)
|
|
6453
|
+
|
|
6454
|
+
|
|
6182
6455
|
def _refresh_claude_observed_activity(row: dict, project: str | None, claude_uuid: str | None) -> None:
|
|
6183
6456
|
"""Use transcript/turn-state evidence to correct stale PG heartbeats."""
|
|
6184
6457
|
observed = int(row.get("last_heartbeat") or 0)
|
|
@@ -6539,6 +6812,10 @@ def _codex_control_overlay(row: dict, observed_mtime: float | None = None, *, ve
|
|
|
6539
6812
|
state_payload = _codex_turn_state_payload(row.get("native_id") or "", apply_boundary=False)
|
|
6540
6813
|
can_send = bool(tty)
|
|
6541
6814
|
can_signal = bool(pid)
|
|
6815
|
+
if tty:
|
|
6816
|
+
row["terminal_tty"] = tty
|
|
6817
|
+
if pid:
|
|
6818
|
+
row["pid"] = pid
|
|
6542
6819
|
caps = set(row.get("capabilities") or [])
|
|
6543
6820
|
if state_payload or can_send or can_signal:
|
|
6544
6821
|
caps.add("live_state")
|
|
@@ -6608,6 +6885,8 @@ def _codex_pending_registry_rows(seen: set[str], live_only: bool, active_within_
|
|
|
6608
6885
|
"last_heartbeat": int(heartbeat or _time.time()),
|
|
6609
6886
|
"stale_seconds": stale_seconds,
|
|
6610
6887
|
"source_freshness": "registry_stale_process_alive" if heartbeat < cutoff and process_alive else "registry_live",
|
|
6888
|
+
"terminal_tty": tty,
|
|
6889
|
+
"pid": pid,
|
|
6611
6890
|
"first_prompt": None,
|
|
6612
6891
|
"state": "running",
|
|
6613
6892
|
"tool": None,
|
|
@@ -6700,7 +6979,7 @@ def _list_codex_sessions(live_only: bool, active_within_min: int) -> list[dict]:
|
|
|
6700
6979
|
|
|
6701
6980
|
|
|
6702
6981
|
def _list_codex_sessions_uncached(live_only: bool, active_within_min: int) -> list[dict]:
|
|
6703
|
-
"""
|
|
6982
|
+
"""Codex provider backed by transcripts plus live terminal discovery."""
|
|
6704
6983
|
index = _codex_index_map()
|
|
6705
6984
|
history = _codex_history_map()
|
|
6706
6985
|
cutoff = _time.time() - max(1, active_within_min) * 60
|
|
@@ -6760,12 +7039,14 @@ def _list_codex_sessions_uncached(live_only: bool, active_within_min: int) -> li
|
|
|
6760
7039
|
row["tool"] = state_payload.get("tool")
|
|
6761
7040
|
row["turn_started_at"] = state_payload.get("started_at")
|
|
6762
7041
|
row["effort"] = state_payload.get("effort")
|
|
6763
|
-
rows.append(_codex_control_overlay(row, st.st_mtime, verify_process=
|
|
7042
|
+
rows.append(_codex_control_overlay(row, st.st_mtime, verify_process=True))
|
|
6764
7043
|
if len(rows) >= 50:
|
|
6765
7044
|
break
|
|
7045
|
+
_codex_register_terminal_only_rows(seen)
|
|
6766
7046
|
rows.extend(_codex_pending_registry_rows(seen, live_only, active_within_min))
|
|
6767
7047
|
if not live_only:
|
|
6768
7048
|
rows.extend(_codex_recent_closed_registry_rows(seen, active_within_min))
|
|
7049
|
+
rows = _collapse_live_session_rows_by_terminal(rows)
|
|
6769
7050
|
rows.sort(
|
|
6770
7051
|
key=lambda r: (
|
|
6771
7052
|
1 if (r.get("controllability") or {}).get("can_terminate") else 0,
|
|
@@ -8898,6 +9179,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
8898
9179
|
for row in _list_codex_sessions(live_only=False, active_within_min=active_within_min):
|
|
8899
9180
|
rows.append(self._decorate_session_lifecycle_row(row))
|
|
8900
9181
|
|
|
9182
|
+
rows = _collapse_live_session_rows_by_terminal(rows)
|
|
8901
9183
|
rows.sort(key=lambda r: int(r.get("last_heartbeat") or 0), reverse=True)
|
|
8902
9184
|
rows = rows[:max(1, min(int(limit or 200), 500))]
|
|
8903
9185
|
for row in rows:
|
|
@@ -9073,6 +9355,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9073
9355
|
self._decorate_session_lifecycle_row(row)
|
|
9074
9356
|
for row in _list_codex_sessions(live_only=live_only, active_within_min=within_min)
|
|
9075
9357
|
]
|
|
9358
|
+
rows = _collapse_live_session_rows_by_terminal(rows)
|
|
9076
9359
|
_record_sessions_scan(rows)
|
|
9077
9360
|
body = json.dumps({"count": len(rows), "items": rows}).encode()
|
|
9078
9361
|
self.send_response(200)
|
|
@@ -9082,14 +9365,11 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9082
9365
|
self.wfile.write(body)
|
|
9083
9366
|
return
|
|
9084
9367
|
|
|
9085
|
-
# Live filter semantics (live_only):
|
|
9086
|
-
# claude_uuid
|
|
9087
|
-
#
|
|
9088
|
-
#
|
|
9089
|
-
|
|
9090
|
-
# at the Sentinel daemon on :9100 instead of us). The kill -0 GC
|
|
9091
|
-
# below catches process-DEAD rows; this catches
|
|
9092
|
-
# process-alive-but-mute rows. Defaults to within_min=60.
|
|
9368
|
+
# Live filter semantics (live_only): keep sessions with either a fresh
|
|
9369
|
+
# claude_uuid-backed hook row or a live Claude process on the recorded
|
|
9370
|
+
# terminal. The freshness gate hides mute zombie rows, while terminal
|
|
9371
|
+
# discovery keeps already-open Claude windows controllable.
|
|
9372
|
+
_claude_register_terminal_only_rows(set())
|
|
9093
9373
|
backend = _claude_sessions_backend()
|
|
9094
9374
|
try:
|
|
9095
9375
|
backend_rows = backend.sessions_rows(live_only, within_min)
|
|
@@ -9163,8 +9443,11 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9163
9443
|
self._decorate_session_lifecycle_row(row)
|
|
9164
9444
|
for row in _list_codex_sessions(live_only=live_only, active_within_min=within_min)
|
|
9165
9445
|
)
|
|
9446
|
+
rows = _collapse_live_session_rows_by_terminal(rows)
|
|
9166
9447
|
rows.sort(key=lambda r: int(r.get("last_heartbeat") or 0), reverse=True)
|
|
9167
9448
|
rows = rows[:50]
|
|
9449
|
+
else:
|
|
9450
|
+
rows = _collapse_live_session_rows_by_terminal(rows)
|
|
9168
9451
|
|
|
9169
9452
|
_record_sessions_scan(rows)
|
|
9170
9453
|
body = json.dumps({"count": len(rows), "items": rows}).encode()
|
|
@@ -9204,8 +9487,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9204
9487
|
"fresh": sum(1 for row in claude_live if now - int(row.get("last_heartbeat") or 0) < 120),
|
|
9205
9488
|
"stale": sum(1 for row in claude_live if now - int(row.get("last_heartbeat") or 0) >= 120),
|
|
9206
9489
|
"notes": [
|
|
9207
|
-
"
|
|
9208
|
-
"
|
|
9490
|
+
"Uses claude_uuid-backed hook rows plus live terminal discovery.",
|
|
9491
|
+
"Terminal-only rows are controllable even before a transcript id is known.",
|
|
9209
9492
|
],
|
|
9210
9493
|
},
|
|
9211
9494
|
{
|
|
@@ -9304,7 +9587,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9304
9587
|
|
|
9305
9588
|
def snapshot_payload(rows: list[dict]) -> tuple[bytes, str]:
|
|
9306
9589
|
degraded = self._sessions_backend_degradation()
|
|
9307
|
-
|
|
9590
|
+
source = _sessions_stream_source()
|
|
9591
|
+
body: dict = {"source": source, "items": rows, "ts": _time.time()}
|
|
9308
9592
|
if degraded is not None:
|
|
9309
9593
|
body["degraded"] = degraded
|
|
9310
9594
|
digest = hashlib.sha256(
|
|
@@ -9315,6 +9599,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9315
9599
|
try:
|
|
9316
9600
|
# Emit initial snapshot immediately so the iPhone never paints
|
|
9317
9601
|
# an empty Dashboard while waiting for the first poll.
|
|
9602
|
+
# Initial snapshot body contract: {"items": initial}.
|
|
9318
9603
|
initial = collect_live()
|
|
9319
9604
|
payload, last_hash = snapshot_payload(initial)
|
|
9320
9605
|
self.wfile.write(b"event: snapshot\ndata: " + payload + b"\n\n")
|
|
@@ -9756,10 +10041,18 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9756
10041
|
def _session_live_terminal_tail(self, raw_session: str, since: int) -> dict | None:
|
|
9757
10042
|
provider, native_id = _parse_agent_session_ref(raw_session)
|
|
9758
10043
|
since = max(0, int(since or 0))
|
|
9759
|
-
|
|
10044
|
+
try:
|
|
10045
|
+
broker_found = self._broker_session_for(raw_session)
|
|
10046
|
+
except Exception as e:
|
|
10047
|
+
broker_found = None
|
|
10048
|
+
self.log_message("session-live terminal broker lookup failed for %s: %s", raw_session, str(e)[:200])
|
|
9760
10049
|
if broker_found and PTY_BROKER:
|
|
9761
10050
|
broker_id, _ = broker_found
|
|
9762
|
-
|
|
10051
|
+
try:
|
|
10052
|
+
tail = PTY_BROKER.raw_tail(broker_id, since=since)
|
|
10053
|
+
except Exception as e:
|
|
10054
|
+
self.log_message("session-live broker tail failed for %s: %s", raw_session, str(e)[:200])
|
|
10055
|
+
tail = None
|
|
9763
10056
|
if tail is None:
|
|
9764
10057
|
return None
|
|
9765
10058
|
data, next_offset, total_bytes, reset = tail
|
|
@@ -9952,22 +10245,21 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9952
10245
|
if not emit("turn_state", turn, source="turn-state"):
|
|
9953
10246
|
return
|
|
9954
10247
|
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
return
|
|
9962
|
-
_time.sleep(0.05)
|
|
9963
|
-
continue
|
|
9964
|
-
|
|
9965
|
-
for receipt_event in _session_live_control_receipts_since(session_id, receipt_seq):
|
|
10248
|
+
try:
|
|
10249
|
+
receipt_events = _session_live_control_receipts_since(session_id, receipt_seq)
|
|
10250
|
+
except Exception as e:
|
|
10251
|
+
receipt_events = []
|
|
10252
|
+
self.log_message("session-live control receipts failed for %s: %s", session_id, str(e)[:200])
|
|
10253
|
+
for receipt_event in receipt_events:
|
|
9966
10254
|
receipt_seq = max(receipt_seq, int(receipt_event.get("receipt_seq") or 0))
|
|
9967
10255
|
if not emit("control_receipt", receipt_event, source="control-receipts"):
|
|
9968
10256
|
return
|
|
9969
10257
|
|
|
9970
|
-
|
|
10258
|
+
try:
|
|
10259
|
+
terminal_payload = self._session_live_terminal_tail(session_id, terminal_offset)
|
|
10260
|
+
except Exception as e:
|
|
10261
|
+
terminal_payload = None
|
|
10262
|
+
self.log_message("session-live terminal tail failed for %s: %s", session_id, str(e)[:200])
|
|
9971
10263
|
if terminal_payload is not None:
|
|
9972
10264
|
if terminal_payload.get("reset"):
|
|
9973
10265
|
terminal_offset = 0
|
|
@@ -10118,7 +10410,14 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10118
10410
|
|
|
10119
10411
|
def _terminal_stream_truth_for_session(self, raw_session: str) -> dict:
|
|
10120
10412
|
provider, native_id = _parse_agent_session_ref(raw_session)
|
|
10121
|
-
|
|
10413
|
+
try:
|
|
10414
|
+
source = _terminal_surface_source(raw_session)
|
|
10415
|
+
except Exception as e:
|
|
10416
|
+
source = {
|
|
10417
|
+
"available": False,
|
|
10418
|
+
"source": "unavailable",
|
|
10419
|
+
"reason": str(e)[:160] or "terminal_surface_source_failed",
|
|
10420
|
+
}
|
|
10122
10421
|
backend = source.get("source")
|
|
10123
10422
|
byte_stream_available = bool(source.get("available")) and backend in {"broker_vt", "script_capture"}
|
|
10124
10423
|
try:
|
|
@@ -10587,9 +10886,6 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10587
10886
|
if not native_id:
|
|
10588
10887
|
return None
|
|
10589
10888
|
qualified = _qualified_session_id(provider, native_id)
|
|
10590
|
-
session = PTY_BROKER.get(qualified)
|
|
10591
|
-
if session:
|
|
10592
|
-
return qualified, session
|
|
10593
10889
|
if provider == "codex":
|
|
10594
10890
|
reg = _agent_registry_get("codex", native_id) or {}
|
|
10595
10891
|
try:
|
|
@@ -10598,26 +10894,61 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10598
10894
|
metadata = {}
|
|
10599
10895
|
broker_id = str(metadata.get("broker_id") or "").strip()
|
|
10600
10896
|
if broker_id:
|
|
10601
|
-
|
|
10897
|
+
try:
|
|
10898
|
+
session = PTY_BROKER.get(broker_id)
|
|
10899
|
+
except Exception:
|
|
10900
|
+
session = None
|
|
10602
10901
|
if session:
|
|
10603
|
-
|
|
10902
|
+
try:
|
|
10903
|
+
PTY_BROKER.register_alias(qualified, _broker_session_id(session))
|
|
10904
|
+
except Exception:
|
|
10905
|
+
pass
|
|
10604
10906
|
return qualified, session
|
|
10605
10907
|
tty = reg.get("terminal_tty") or ""
|
|
10606
10908
|
if tty:
|
|
10607
|
-
|
|
10909
|
+
try:
|
|
10910
|
+
session = PTY_BROKER.get_by_tty(tty)
|
|
10911
|
+
except Exception:
|
|
10912
|
+
session = None
|
|
10608
10913
|
if session:
|
|
10609
|
-
|
|
10914
|
+
try:
|
|
10915
|
+
PTY_BROKER.register_alias(qualified, _broker_session_id(session))
|
|
10916
|
+
except Exception:
|
|
10917
|
+
pass
|
|
10610
10918
|
return qualified, session
|
|
10611
10919
|
if provider == "claude":
|
|
10612
10920
|
session_id = _claude_native_session_id(raw_session)
|
|
10613
10921
|
if not session_id:
|
|
10614
10922
|
return None
|
|
10923
|
+
reg = _agent_registry_get("claude", session_id) or {}
|
|
10924
|
+
try:
|
|
10925
|
+
metadata = json.loads(reg.get("metadata_json") or "{}")
|
|
10926
|
+
except Exception:
|
|
10927
|
+
metadata = {}
|
|
10928
|
+
broker_id = str(metadata.get("broker_id") or "").strip()
|
|
10929
|
+
if broker_id:
|
|
10930
|
+
try:
|
|
10931
|
+
session = PTY_BROKER.get(broker_id)
|
|
10932
|
+
except Exception:
|
|
10933
|
+
session = None
|
|
10934
|
+
if session:
|
|
10935
|
+
try:
|
|
10936
|
+
PTY_BROKER.register_alias(qualified, _broker_session_id(session))
|
|
10937
|
+
except Exception:
|
|
10938
|
+
pass
|
|
10939
|
+
return qualified, session
|
|
10615
10940
|
tty = self._lookup_terminal_tty(session_id)
|
|
10616
10941
|
if tty:
|
|
10617
|
-
|
|
10942
|
+
try:
|
|
10943
|
+
session = PTY_BROKER.get_by_tty(tty)
|
|
10944
|
+
except Exception:
|
|
10945
|
+
session = None
|
|
10618
10946
|
if session:
|
|
10619
10947
|
qualified = _qualified_session_id("claude", session_id)
|
|
10620
|
-
|
|
10948
|
+
try:
|
|
10949
|
+
PTY_BROKER.register_alias(qualified, _broker_session_id(session))
|
|
10950
|
+
except Exception:
|
|
10951
|
+
pass
|
|
10621
10952
|
return qualified, session
|
|
10622
10953
|
return None
|
|
10623
10954
|
|
|
@@ -11665,6 +11996,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
11665
11996
|
cached = _runtime_snapshot_cache.get(cache_key)
|
|
11666
11997
|
if cached is not None and now - cached[0] < RUNTIME_SNAPSHOT_CACHE_SECONDS:
|
|
11667
11998
|
return _copy_cache_value(cached[1])
|
|
11999
|
+
_claude_register_terminal_only_rows(set())
|
|
11668
12000
|
backend_rows = _claude_sessions_backend().collect_rows(since_min, live_only, limit)
|
|
11669
12001
|
|
|
11670
12002
|
rows: list[dict] = []
|
|
@@ -14633,10 +14965,9 @@ Worker instructions:
|
|
|
14633
14965
|
self._send_json({"ok": True, "handoff_id": handoff_id, "composeDraft": compose_draft})
|
|
14634
14966
|
|
|
14635
14967
|
def _handle_spawn_session(self, q):
|
|
14636
|
-
"""Spawn a new Claude/Codex session.
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
PAIRLING_SPAWN_BACKEND=terminal_app for rollback/debugging.
|
|
14968
|
+
"""Spawn a new Claude/Codex session. User-facing direct spawns open a
|
|
14969
|
+
visible Terminal.app tab. The broker remains for aperture_cli launches
|
|
14970
|
+
and PAIRLING_SPAWN_BACKEND=broker rollback/debugging.
|
|
14640
14971
|
|
|
14641
14972
|
Security:
|
|
14642
14973
|
- Project path must be absolute and exist on disk.
|
|
@@ -14742,7 +15073,7 @@ Worker instructions:
|
|
|
14742
15073
|
}, status=400)
|
|
14743
15074
|
return
|
|
14744
15075
|
|
|
14745
|
-
if os.environ.get("PAIRLING_SPAWN_BACKEND", "
|
|
15076
|
+
if launch_strategy == "aperture_cli" or os.environ.get("PAIRLING_SPAWN_BACKEND", "terminal_app").lower() == "broker":
|
|
14746
15077
|
self._handle_spawn_session_broker(
|
|
14747
15078
|
project,
|
|
14748
15079
|
provider,
|
|
@@ -14751,13 +15082,6 @@ Worker instructions:
|
|
|
14751
15082
|
)
|
|
14752
15083
|
return
|
|
14753
15084
|
|
|
14754
|
-
if launch_strategy == "aperture_cli":
|
|
14755
|
-
self._send_json({
|
|
14756
|
-
"ok": False,
|
|
14757
|
-
"error": "Aperture CLI launch strategy requires the Pairling PTY broker backend",
|
|
14758
|
-
}, status=400)
|
|
14759
|
-
return
|
|
14760
|
-
|
|
14761
15085
|
capture_id = ""
|
|
14762
15086
|
capture_log_path: Path | None = None
|
|
14763
15087
|
if provider == "codex":
|
|
@@ -14772,7 +15096,11 @@ Worker instructions:
|
|
|
14772
15096
|
else:
|
|
14773
15097
|
capture_id = secrets.token_hex(12)
|
|
14774
15098
|
capture_log_path = TERMINAL_CAPTURE_DIR / f"claude-{capture_id}.log"
|
|
14775
|
-
inner =
|
|
15099
|
+
inner = (
|
|
15100
|
+
f"cd {shlex.quote(project)} && "
|
|
15101
|
+
f"PAIRLING_PHONE_SESSION=1 "
|
|
15102
|
+
f"exec claude --settings {shlex.quote(str(SPAWN_SETTINGS_PATH))}"
|
|
15103
|
+
)
|
|
14776
15104
|
shell_cmd = _terminal_script_command(capture_log_path, inner)
|
|
14777
15105
|
as_escaped_cmd = _as_escape(shell_cmd)
|
|
14778
15106
|
|
|
@@ -14801,41 +15129,37 @@ Worker instructions:
|
|
|
14801
15129
|
if len(parts) >= 2:
|
|
14802
15130
|
tty = parts[1].strip()
|
|
14803
15131
|
pid = 0
|
|
14804
|
-
native_id =
|
|
14805
|
-
if
|
|
14806
|
-
native_id = "pending-" + secrets.token_hex(8)
|
|
15132
|
+
native_id = "pending-" + secrets.token_hex(8)
|
|
15133
|
+
if result.get("ok"):
|
|
14807
15134
|
_time.sleep(0.75)
|
|
14808
|
-
pid = _pid_for_tty_command(tty,
|
|
15135
|
+
pid = _pid_for_tty_command(tty, provider) if tty else 0
|
|
14809
15136
|
if tty and capture_log_path is not None:
|
|
14810
15137
|
_write_terminal_capture_mapping(
|
|
14811
15138
|
tty,
|
|
14812
15139
|
capture_log_path,
|
|
14813
|
-
provider=
|
|
15140
|
+
provider=provider,
|
|
14814
15141
|
project=project,
|
|
14815
15142
|
capture_id=capture_id,
|
|
14816
15143
|
)
|
|
14817
15144
|
_agent_registry_upsert(
|
|
14818
|
-
|
|
15145
|
+
provider,
|
|
14819
15146
|
native_id,
|
|
14820
15147
|
project,
|
|
14821
15148
|
pid=pid,
|
|
14822
15149
|
terminal_tty=tty,
|
|
14823
15150
|
metadata={
|
|
14824
|
-
"spawned_by": "
|
|
15151
|
+
"spawned_by": "pairling",
|
|
15152
|
+
"launch_strategy": "direct_pairling",
|
|
15153
|
+
"launch_visibility": "visible_terminal",
|
|
14825
15154
|
"terminal_log": str(capture_log_path) if capture_log_path else None,
|
|
14826
15155
|
"capture_backend": "script" if capture_log_path else None,
|
|
15156
|
+
"terminal_source": "terminal_app_contents",
|
|
14827
15157
|
"capture_id": capture_id or None,
|
|
14828
15158
|
},
|
|
15159
|
+
working_on=f"New {provider.title()} session",
|
|
14829
15160
|
)
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
_write_terminal_capture_mapping(
|
|
14833
|
-
tty,
|
|
14834
|
-
capture_log_path,
|
|
14835
|
-
provider="claude",
|
|
14836
|
-
project=project,
|
|
14837
|
-
capture_id=capture_id,
|
|
14838
|
-
)
|
|
15161
|
+
if provider == "codex":
|
|
15162
|
+
_write_agent_turn_state("codex", native_id, "idle", event="spawn")
|
|
14839
15163
|
|
|
14840
15164
|
# Audit log — append-only, JSONL, includes failures.
|
|
14841
15165
|
try:
|
|
@@ -14867,20 +15191,21 @@ Worker instructions:
|
|
|
14867
15191
|
self.wfile.write(body)
|
|
14868
15192
|
return
|
|
14869
15193
|
|
|
14870
|
-
|
|
15194
|
+
self._send_json({
|
|
14871
15195
|
"ok": True,
|
|
14872
15196
|
"project": project,
|
|
14873
15197
|
"provider": provider,
|
|
14874
15198
|
"native_id": native_id,
|
|
15199
|
+
"session_id": _qualified_session_id(provider, native_id),
|
|
14875
15200
|
"tty": tty,
|
|
14876
15201
|
"pid": pid,
|
|
14877
15202
|
"terminal_log": str(capture_log_path) if capture_log_path else None,
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
15203
|
+
"capture_backend": "script" if capture_log_path else None,
|
|
15204
|
+
"terminal_source": "terminal_app_contents",
|
|
15205
|
+
"launch_strategy": "direct_pairling",
|
|
15206
|
+
"launch_visibility": "visible_terminal",
|
|
15207
|
+
"attach_command": None,
|
|
15208
|
+
})
|
|
14884
15209
|
|
|
14885
15210
|
def _session_context_for_workflow(self, raw_session: str) -> dict | None:
|
|
14886
15211
|
provider, native_id = _parse_agent_session_ref(raw_session)
|
|
@@ -15244,7 +15569,7 @@ Worker instructions:
|
|
|
15244
15569
|
self.wfile.write(body)
|
|
15245
15570
|
return
|
|
15246
15571
|
|
|
15247
|
-
if os.environ.get("PAIRLING_RESUME_BACKEND", "
|
|
15572
|
+
if os.environ.get("PAIRLING_RESUME_BACKEND", "terminal_app").lower() == "broker":
|
|
15248
15573
|
self._handle_resume_session_broker(provider=provider, project=project, native_id=native_id, prompt=prompt)
|
|
15249
15574
|
return
|
|
15250
15575
|
|
|
@@ -15299,6 +15624,8 @@ Worker instructions:
|
|
|
15299
15624
|
"resume_target": native_id,
|
|
15300
15625
|
"terminal_log": str(capture_log_path),
|
|
15301
15626
|
"capture_backend": "script",
|
|
15627
|
+
"terminal_source": "terminal_app_contents",
|
|
15628
|
+
"launch_visibility": "visible_terminal",
|
|
15302
15629
|
"capture_id": capture_id,
|
|
15303
15630
|
},
|
|
15304
15631
|
)
|
|
@@ -15330,7 +15657,7 @@ Worker instructions:
|
|
|
15330
15657
|
self.end_headers()
|
|
15331
15658
|
self.wfile.write(body)
|
|
15332
15659
|
return
|
|
15333
|
-
|
|
15660
|
+
self._send_json({
|
|
15334
15661
|
"ok": True,
|
|
15335
15662
|
"provider": "codex",
|
|
15336
15663
|
"native_id": native_id,
|
|
@@ -15341,13 +15668,9 @@ Worker instructions:
|
|
|
15341
15668
|
"terminal_log": str(capture_log_path),
|
|
15342
15669
|
"capture_backend": "script",
|
|
15343
15670
|
"terminal_source": "terminal_app_contents",
|
|
15671
|
+
"launch_visibility": "visible_terminal",
|
|
15344
15672
|
"attach_command": None,
|
|
15345
|
-
})
|
|
15346
|
-
self.send_response(200)
|
|
15347
|
-
self.send_header("Content-Type", "application/json")
|
|
15348
|
-
self.send_header("Content-Length", str(len(body)))
|
|
15349
|
-
self.end_headers()
|
|
15350
|
-
self.wfile.write(body)
|
|
15673
|
+
})
|
|
15351
15674
|
|
|
15352
15675
|
def _send_text_to_codex_registry(self, native_id: str, text: str, receipt_context: dict | None = None) -> None:
|
|
15353
15676
|
# Precondition: callers pass text through _sanitize_terminal_text_input.
|
|
@@ -1366,10 +1366,11 @@ def default_pair_route(port_number: int) -> dict:
|
|
|
1366
1366
|
value = os.environ.get(key)
|
|
1367
1367
|
if value:
|
|
1368
1368
|
return {"base_url": value, "source": "explicit_override", "status": "override"}
|
|
1369
|
-
#
|
|
1370
|
-
#
|
|
1371
|
-
|
|
1372
|
-
|
|
1369
|
+
# First-pair bootstrap: prefer LAN when it exists. iOS blocks plain HTTP to
|
|
1370
|
+
# a tailnet IP before the embedded Pairling Connect route is ready.
|
|
1371
|
+
lan_ip = detected_lan_ip()
|
|
1372
|
+
if lan_ip:
|
|
1373
|
+
return {"base_url": f"http://{lan_ip}:{port_number}", "source": "lan", "status": "fallback", "kind": "lan"}
|
|
1373
1374
|
route = ready_connectd_route()
|
|
1374
1375
|
if route:
|
|
1375
1376
|
return {
|
|
@@ -1378,9 +1379,6 @@ def default_pair_route(port_number: int) -> dict:
|
|
|
1378
1379
|
"status": route["status"],
|
|
1379
1380
|
"kind": route["kind"],
|
|
1380
1381
|
}
|
|
1381
|
-
lan_ip = detected_lan_ip()
|
|
1382
|
-
if lan_ip:
|
|
1383
|
-
return {"base_url": f"http://{lan_ip}:{port_number}", "source": "lan", "status": "fallback", "kind": "lan"}
|
|
1384
1382
|
if os.environ.get("PAIRLING_DISABLE_BONJOUR") != "1" and os.environ.get("PAIRLING_TEST_DISABLE_BONJOUR") != "1":
|
|
1385
1383
|
return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback", "kind": "bonjour"}
|
|
1386
1384
|
tailnet_ip = detected_tailnet_ip()
|
package/payload-manifest.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"connectd": {
|
|
3
3
|
"darwin-arm64": {
|
|
4
|
-
"sha256": "
|
|
4
|
+
"sha256": "2bd3903963b0395cc2af25c0ca88450873b8f878fa8b0fcf5b70283fde21602f",
|
|
5
5
|
"team_id": "965AVD34A3"
|
|
6
6
|
},
|
|
7
7
|
"darwin-x64": {
|
|
8
|
-
"sha256": "
|
|
8
|
+
"sha256": "301fe95148dbc5a5144165802319d817a4a5c8f1492d0b5a11ef101f4fffbc9e",
|
|
9
9
|
"team_id": "965AVD34A3"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "payload/mac/SOURCE_REVISION",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "48e3c96b6c22c6c3424b1fbcebbbee0e23f0e61535ac6b487dec510a496cb6ef"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "payload/mac/VERSION",
|
|
27
|
-
"sha256": "
|
|
27
|
+
"sha256": "bbab81dca58f2b892db293acbba7fe5731667b67876f6812e3e7b9e2253a5c51"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "payload/mac/companiond/app_attest_lan.py",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
"path": "payload/mac/companiond/pairlingd.py",
|
|
99
|
-
"sha256": "
|
|
99
|
+
"sha256": "ad3d9929966d41778de40be0ae908ea22ec49f811f9a6aceabd606aa41b6a2db"
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
"path": "payload/mac/companiond/providers/__init__.py",
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
},
|
|
233
233
|
{
|
|
234
234
|
"path": "payload/mac/connectd/internal/gateway/proxy.go",
|
|
235
|
-
"sha256": "
|
|
235
|
+
"sha256": "9d57225667f5ab025286c6714864762cbd31a8bdaf176f2b5679a26ca7144f74"
|
|
236
236
|
},
|
|
237
237
|
{
|
|
238
238
|
"path": "payload/mac/connectd/internal/gateway/proxy_test.go",
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
},
|
|
273
273
|
{
|
|
274
274
|
"path": "payload/mac/install/install-runtime.sh",
|
|
275
|
-
"sha256": "
|
|
275
|
+
"sha256": "2575f5ae2aeb03d6bce5ae1194498ac34d14d09c993c4a1a4474e187cae10618"
|
|
276
276
|
},
|
|
277
277
|
{
|
|
278
278
|
"path": "payload/mac/install/psk_dependency_check.py",
|
|
@@ -296,8 +296,8 @@
|
|
|
296
296
|
}
|
|
297
297
|
],
|
|
298
298
|
"package": "pairling",
|
|
299
|
-
"package_version": "0.2.
|
|
299
|
+
"package_version": "0.2.11",
|
|
300
300
|
"schema_version": 1,
|
|
301
301
|
"source_dirty": false,
|
|
302
|
-
"source_revision": "
|
|
302
|
+
"source_revision": "83c69e1"
|
|
303
303
|
}
|