Skip to content

Commit 5885157

Browse files
illeatmyhatclaude
andcommitted
feat(platform-integrations): close Bob provenance via a session-id bridge
Bob (a Gemini-CLI fork) exposes no session-id env var to tool subprocesses, so audit_recall.py minted a uuid that could never be tied to Bob's saved trajectory — provenance resolved the entity but never the trajectory. But Bob DOES store the real session id: ~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json carries it in a 'sessionId' field (verified: projectHash == sha256(cwd), and the filename's trailing block is the id's first segment). Bridge both ends to that real id: - audit_recall.py: when under Bob (gated on BOBSHELL_CLI) with no Claude/Codex env id, recover sessionId from the newest chat file under sha256(cwd) instead of minting. Inert on every other host. - provenance.locate_trajectory: add a Bob branch that matches the chat file whose 'sessionId' equals the audited id (filename-prefiltered, body field authoritative). Validated headlessly end-to-end: Bob recall logs the real sessionId, provenance fully resolves (entity + trajectory), influence verdict records, candidates dedup to empty. All three platforms now close discover→save→recall→audit→ provenance. +6 unit tests (recover, newest-wins, inert-without-BOBSHELL_CLI, env-id-precedence, locate, body-mismatch-rejected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 01bb0a3 commit 5885157

12 files changed

Lines changed: 432 additions & 0 deletions

File tree

platform-integrations/bob/evolve-lite/lib/evolve-lite/audit_recall.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import annotations
1717

18+
import hashlib
1819
import json
1920
import os
2021
import sys
@@ -28,12 +29,48 @@ def _evolve_dir() -> Path:
2829
return Path(env) if env else Path.cwd() / ".evolve"
2930

3031

32+
def _bob_session_id() -> str | None:
33+
"""Recover Bob's real session id for the current run.
34+
35+
Bob (a Gemini-CLI fork) exposes no session-id environment variable to tool
36+
subprocesses, but it writes the live session to
37+
``~/.bob/tmp/<sha256(cwd)>/chats/session-<ts>-<sid8>.json`` with a real
38+
``sessionId`` field (the filename's trailing segment is that id's first
39+
block). Recovering it lets `provenance` tie this recall to the saved
40+
trajectory instead of an opaque minted uuid. Gated on ``BOBSHELL_CLI`` so it
41+
is inert on every other host. Returns the id, or ``None`` when not under Bob
42+
or no chat file is found (caller then mints a uuid)."""
43+
if not os.environ.get("BOBSHELL_CLI"):
44+
return None
45+
try:
46+
project_hash = hashlib.sha256(os.getcwd().encode()).hexdigest()
47+
chats = Path.home() / ".bob" / "tmp" / project_hash / "chats"
48+
files = sorted(
49+
chats.glob("session-*.json"),
50+
key=lambda p: p.stat().st_mtime,
51+
reverse=True,
52+
)
53+
for chat in files:
54+
try:
55+
sid = json.loads(chat.read_text(encoding="utf-8")).get("sessionId")
56+
except (OSError, json.JSONDecodeError):
57+
continue
58+
if sid:
59+
return str(sid)
60+
except OSError:
61+
return None
62+
return None
63+
64+
3165
def _session_id() -> tuple[str, bool]:
3266
"""Return (session_id, self_minted)."""
3367
for var in ("CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"):
3468
val = os.environ.get(var)
3569
if val:
3670
return val, False
71+
bob_sid = _bob_session_id()
72+
if bob_sid:
73+
return bob_sid, False
3774
return str(uuid.uuid4()), True
3875

3976

platform-integrations/bob/evolve-lite/skills/evolve-lite-provenance/scripts/provenance.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
9999
Codex names each rollout file with the thread id as the trailing segment,
100100
so a recursive glob for ``*<sid>*.jsonl`` under ``~/.codex/sessions/``
101101
finds it regardless of date directory.
102+
4. NEW native Bob transcript: ``~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json``.
103+
Bob carries the real session id in the file's ``sessionId`` field (the
104+
filename's trailing segment is that id's first block). ``audit_recall.py``
105+
logs that real id, so match the chat file whose ``sessionId`` equals it.
102106
103107
Native discovery makes provenance work in the hookless world where no
104108
``.evolve/trajectories/`` file is ever written. Each native step is keyed on
@@ -156,6 +160,26 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
156160
if matches:
157161
return matches[0]
158162

163+
# --- 4. Native Bob transcript -------------------------------------------
164+
# Bob (a Gemini-CLI fork) stores sessions at
165+
# ~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json, each
166+
# carrying a real ``sessionId`` field whose first block is the filename's
167+
# trailing segment. audit_recall.py logs that real sessionId (see its
168+
# ``_bob_session_id``), so match the chat file whose sessionId equals it.
169+
# Glob across project-hash dirs (cheap) and prefilter filenames by the id's
170+
# first block rather than recomputing the hash.
171+
if session_id and "/" not in session_id and "*" not in session_id:
172+
bob_tmp = base / ".bob" / "tmp"
173+
if bob_tmp.is_dir():
174+
sid_head = session_id.split("-", 1)[0]
175+
for chat in sorted(bob_tmp.glob(f"*/chats/session-*{sid_head}*.json")):
176+
try:
177+
data = json.loads(chat.read_text(encoding="utf-8"))
178+
except (OSError, json.JSONDecodeError):
179+
continue
180+
if data.get("sessionId") == session_id:
181+
return chat
182+
159183
return None
160184

161185

platform-integrations/claude/plugins/evolve-lite/lib/evolve-lite/audit_recall.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import annotations
1717

18+
import hashlib
1819
import json
1920
import os
2021
import sys
@@ -28,12 +29,48 @@ def _evolve_dir() -> Path:
2829
return Path(env) if env else Path.cwd() / ".evolve"
2930

3031

32+
def _bob_session_id() -> str | None:
33+
"""Recover Bob's real session id for the current run.
34+
35+
Bob (a Gemini-CLI fork) exposes no session-id environment variable to tool
36+
subprocesses, but it writes the live session to
37+
``~/.bob/tmp/<sha256(cwd)>/chats/session-<ts>-<sid8>.json`` with a real
38+
``sessionId`` field (the filename's trailing segment is that id's first
39+
block). Recovering it lets `provenance` tie this recall to the saved
40+
trajectory instead of an opaque minted uuid. Gated on ``BOBSHELL_CLI`` so it
41+
is inert on every other host. Returns the id, or ``None`` when not under Bob
42+
or no chat file is found (caller then mints a uuid)."""
43+
if not os.environ.get("BOBSHELL_CLI"):
44+
return None
45+
try:
46+
project_hash = hashlib.sha256(os.getcwd().encode()).hexdigest()
47+
chats = Path.home() / ".bob" / "tmp" / project_hash / "chats"
48+
files = sorted(
49+
chats.glob("session-*.json"),
50+
key=lambda p: p.stat().st_mtime,
51+
reverse=True,
52+
)
53+
for chat in files:
54+
try:
55+
sid = json.loads(chat.read_text(encoding="utf-8")).get("sessionId")
56+
except (OSError, json.JSONDecodeError):
57+
continue
58+
if sid:
59+
return str(sid)
60+
except OSError:
61+
return None
62+
return None
63+
64+
3165
def _session_id() -> tuple[str, bool]:
3266
"""Return (session_id, self_minted)."""
3367
for var in ("CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"):
3468
val = os.environ.get(var)
3569
if val:
3670
return val, False
71+
bob_sid = _bob_session_id()
72+
if bob_sid:
73+
return bob_sid, False
3774
return str(uuid.uuid4()), True
3875

3976

platform-integrations/claude/plugins/evolve-lite/skills/evolve-lite/provenance/scripts/provenance.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
9999
Codex names each rollout file with the thread id as the trailing segment,
100100
so a recursive glob for ``*<sid>*.jsonl`` under ``~/.codex/sessions/``
101101
finds it regardless of date directory.
102+
4. NEW native Bob transcript: ``~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json``.
103+
Bob carries the real session id in the file's ``sessionId`` field (the
104+
filename's trailing segment is that id's first block). ``audit_recall.py``
105+
logs that real id, so match the chat file whose ``sessionId`` equals it.
102106
103107
Native discovery makes provenance work in the hookless world where no
104108
``.evolve/trajectories/`` file is ever written. Each native step is keyed on
@@ -156,6 +160,26 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
156160
if matches:
157161
return matches[0]
158162

163+
# --- 4. Native Bob transcript -------------------------------------------
164+
# Bob (a Gemini-CLI fork) stores sessions at
165+
# ~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json, each
166+
# carrying a real ``sessionId`` field whose first block is the filename's
167+
# trailing segment. audit_recall.py logs that real sessionId (see its
168+
# ``_bob_session_id``), so match the chat file whose sessionId equals it.
169+
# Glob across project-hash dirs (cheap) and prefilter filenames by the id's
170+
# first block rather than recomputing the hash.
171+
if session_id and "/" not in session_id and "*" not in session_id:
172+
bob_tmp = base / ".bob" / "tmp"
173+
if bob_tmp.is_dir():
174+
sid_head = session_id.split("-", 1)[0]
175+
for chat in sorted(bob_tmp.glob(f"*/chats/session-*{sid_head}*.json")):
176+
try:
177+
data = json.loads(chat.read_text(encoding="utf-8"))
178+
except (OSError, json.JSONDecodeError):
179+
continue
180+
if data.get("sessionId") == session_id:
181+
return chat
182+
159183
return None
160184

161185

platform-integrations/claw-code/plugins/evolve-lite/lib/evolve-lite/audit_recall.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import annotations
1717

18+
import hashlib
1819
import json
1920
import os
2021
import sys
@@ -28,12 +29,48 @@ def _evolve_dir() -> Path:
2829
return Path(env) if env else Path.cwd() / ".evolve"
2930

3031

32+
def _bob_session_id() -> str | None:
33+
"""Recover Bob's real session id for the current run.
34+
35+
Bob (a Gemini-CLI fork) exposes no session-id environment variable to tool
36+
subprocesses, but it writes the live session to
37+
``~/.bob/tmp/<sha256(cwd)>/chats/session-<ts>-<sid8>.json`` with a real
38+
``sessionId`` field (the filename's trailing segment is that id's first
39+
block). Recovering it lets `provenance` tie this recall to the saved
40+
trajectory instead of an opaque minted uuid. Gated on ``BOBSHELL_CLI`` so it
41+
is inert on every other host. Returns the id, or ``None`` when not under Bob
42+
or no chat file is found (caller then mints a uuid)."""
43+
if not os.environ.get("BOBSHELL_CLI"):
44+
return None
45+
try:
46+
project_hash = hashlib.sha256(os.getcwd().encode()).hexdigest()
47+
chats = Path.home() / ".bob" / "tmp" / project_hash / "chats"
48+
files = sorted(
49+
chats.glob("session-*.json"),
50+
key=lambda p: p.stat().st_mtime,
51+
reverse=True,
52+
)
53+
for chat in files:
54+
try:
55+
sid = json.loads(chat.read_text(encoding="utf-8")).get("sessionId")
56+
except (OSError, json.JSONDecodeError):
57+
continue
58+
if sid:
59+
return str(sid)
60+
except OSError:
61+
return None
62+
return None
63+
64+
3165
def _session_id() -> tuple[str, bool]:
3266
"""Return (session_id, self_minted)."""
3367
for var in ("CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"):
3468
val = os.environ.get(var)
3569
if val:
3670
return val, False
71+
bob_sid = _bob_session_id()
72+
if bob_sid:
73+
return bob_sid, False
3774
return str(uuid.uuid4()), True
3875

3976

platform-integrations/claw-code/plugins/evolve-lite/skills/evolve-lite/provenance/scripts/provenance.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
9999
Codex names each rollout file with the thread id as the trailing segment,
100100
so a recursive glob for ``*<sid>*.jsonl`` under ``~/.codex/sessions/``
101101
finds it regardless of date directory.
102+
4. NEW native Bob transcript: ``~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json``.
103+
Bob carries the real session id in the file's ``sessionId`` field (the
104+
filename's trailing segment is that id's first block). ``audit_recall.py``
105+
logs that real id, so match the chat file whose ``sessionId`` equals it.
102106
103107
Native discovery makes provenance work in the hookless world where no
104108
``.evolve/trajectories/`` file is ever written. Each native step is keyed on
@@ -156,6 +160,26 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
156160
if matches:
157161
return matches[0]
158162

163+
# --- 4. Native Bob transcript -------------------------------------------
164+
# Bob (a Gemini-CLI fork) stores sessions at
165+
# ~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json, each
166+
# carrying a real ``sessionId`` field whose first block is the filename's
167+
# trailing segment. audit_recall.py logs that real sessionId (see its
168+
# ``_bob_session_id``), so match the chat file whose sessionId equals it.
169+
# Glob across project-hash dirs (cheap) and prefilter filenames by the id's
170+
# first block rather than recomputing the hash.
171+
if session_id and "/" not in session_id and "*" not in session_id:
172+
bob_tmp = base / ".bob" / "tmp"
173+
if bob_tmp.is_dir():
174+
sid_head = session_id.split("-", 1)[0]
175+
for chat in sorted(bob_tmp.glob(f"*/chats/session-*{sid_head}*.json")):
176+
try:
177+
data = json.loads(chat.read_text(encoding="utf-8"))
178+
except (OSError, json.JSONDecodeError):
179+
continue
180+
if data.get("sessionId") == session_id:
181+
return chat
182+
159183
return None
160184

161185

platform-integrations/codex/plugins/evolve-lite/lib/evolve-lite/audit_recall.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import annotations
1717

18+
import hashlib
1819
import json
1920
import os
2021
import sys
@@ -28,12 +29,48 @@ def _evolve_dir() -> Path:
2829
return Path(env) if env else Path.cwd() / ".evolve"
2930

3031

32+
def _bob_session_id() -> str | None:
33+
"""Recover Bob's real session id for the current run.
34+
35+
Bob (a Gemini-CLI fork) exposes no session-id environment variable to tool
36+
subprocesses, but it writes the live session to
37+
``~/.bob/tmp/<sha256(cwd)>/chats/session-<ts>-<sid8>.json`` with a real
38+
``sessionId`` field (the filename's trailing segment is that id's first
39+
block). Recovering it lets `provenance` tie this recall to the saved
40+
trajectory instead of an opaque minted uuid. Gated on ``BOBSHELL_CLI`` so it
41+
is inert on every other host. Returns the id, or ``None`` when not under Bob
42+
or no chat file is found (caller then mints a uuid)."""
43+
if not os.environ.get("BOBSHELL_CLI"):
44+
return None
45+
try:
46+
project_hash = hashlib.sha256(os.getcwd().encode()).hexdigest()
47+
chats = Path.home() / ".bob" / "tmp" / project_hash / "chats"
48+
files = sorted(
49+
chats.glob("session-*.json"),
50+
key=lambda p: p.stat().st_mtime,
51+
reverse=True,
52+
)
53+
for chat in files:
54+
try:
55+
sid = json.loads(chat.read_text(encoding="utf-8")).get("sessionId")
56+
except (OSError, json.JSONDecodeError):
57+
continue
58+
if sid:
59+
return str(sid)
60+
except OSError:
61+
return None
62+
return None
63+
64+
3165
def _session_id() -> tuple[str, bool]:
3266
"""Return (session_id, self_minted)."""
3367
for var in ("CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"):
3468
val = os.environ.get(var)
3569
if val:
3670
return val, False
71+
bob_sid = _bob_session_id()
72+
if bob_sid:
73+
return bob_sid, False
3774
return str(uuid.uuid4()), True
3875

3976

platform-integrations/codex/plugins/evolve-lite/skills/evolve-lite/provenance/scripts/provenance.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
9999
Codex names each rollout file with the thread id as the trailing segment,
100100
so a recursive glob for ``*<sid>*.jsonl`` under ``~/.codex/sessions/``
101101
finds it regardless of date directory.
102+
4. NEW native Bob transcript: ``~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json``.
103+
Bob carries the real session id in the file's ``sessionId`` field (the
104+
filename's trailing segment is that id's first block). ``audit_recall.py``
105+
logs that real id, so match the chat file whose ``sessionId`` equals it.
102106
103107
Native discovery makes provenance work in the hookless world where no
104108
``.evolve/trajectories/`` file is ever written. Each native step is keyed on
@@ -156,6 +160,26 @@ def locate_trajectory(session_id, evolve_dir, *, project_root=None, home=None):
156160
if matches:
157161
return matches[0]
158162

163+
# --- 4. Native Bob transcript -------------------------------------------
164+
# Bob (a Gemini-CLI fork) stores sessions at
165+
# ~/.bob/tmp/<sha256(projectpath)>/chats/session-<ts>-<sid8>.json, each
166+
# carrying a real ``sessionId`` field whose first block is the filename's
167+
# trailing segment. audit_recall.py logs that real sessionId (see its
168+
# ``_bob_session_id``), so match the chat file whose sessionId equals it.
169+
# Glob across project-hash dirs (cheap) and prefilter filenames by the id's
170+
# first block rather than recomputing the hash.
171+
if session_id and "/" not in session_id and "*" not in session_id:
172+
bob_tmp = base / ".bob" / "tmp"
173+
if bob_tmp.is_dir():
174+
sid_head = session_id.split("-", 1)[0]
175+
for chat in sorted(bob_tmp.glob(f"*/chats/session-*{sid_head}*.json")):
176+
try:
177+
data = json.loads(chat.read_text(encoding="utf-8"))
178+
except (OSError, json.JSONDecodeError):
179+
continue
180+
if data.get("sessionId") == session_id:
181+
return chat
182+
159183
return None
160184

161185

0 commit comments

Comments
 (0)