e4e2332e01
Files changed: - CHANGES.md - EVALS.md - INSTALL.md - VERSION - instructions/session-setup.md - tools/CONTRACT.md - tools/chemenu/cli.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/run_budget.py - tools/chemenu/session.py - tools/chemenu/telemetry/writer.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_cli.py - tools/chemenu/tests/test_run_budget.py - tools/chemenu/tests/test_telemetry_emit.py
93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
"""Session identity, shared by the budget gate and the trace emitter.
|
|
|
|
One definition, because the two must agree: if telemetry grouped events
|
|
differently from the way the budget counts calls, a trace could not be read
|
|
against the gate that refused it.
|
|
|
|
Three-step fallback chain, in order:
|
|
|
|
1. `WIKITOOL_SESSION_ID`, if the caller set one explicitly. Skills set it so a
|
|
session is scoped to a task rather than to a terminal window (see
|
|
instructions/session-setup.md).
|
|
2. A harness's own session variable, from `HARNESS_ENV_VARS` below - checked
|
|
only when nothing set the variable above.
|
|
3. `os.getppid()` - the parent process of this CLI invocation. On a harness
|
|
that runs every tool call in a freshly initialised shell (Claude Code's
|
|
Bash tool does), this is a new "session" per call and neither the
|
|
iteration-budget gate's ceiling nor its loop-breaker can ever trip - see
|
|
Gitea #110, which measured a 33-call run splitting into 21 telemetry
|
|
buckets under this fallback alone.
|
|
|
|
Step 2 is what closes that gap without asking every skill to `export` a
|
|
variable a harness already re-derives per call: `CLAUDE_CODE_SESSION_ID` is
|
|
stable across a Claude Code session's tool calls (verified 2026-09-16,
|
|
against a live session, across separate Bash invocations - the shell's own
|
|
PID changed on every call, this variable did not) and is **exactly** the id
|
|
the `UserPromptSubmit` hook writes into a trace's `session.start` and
|
|
`prompt.submitted` events. Using it unmodified as the budget/telemetry key -
|
|
no prefix, no rewriting - is what lets the hook's events and this module's
|
|
events land in the same bucket.
|
|
|
|
`HARNESS_ENV_VARS` only ever grows by a verified entry: a variable a real
|
|
session was observed setting, confirmed to be the same id a harness's own
|
|
hooks use elsewhere in a trace. A guessed name that happens to exist and
|
|
means something else would be worse than the `getppid()` fallback it would
|
|
replace - it would look like a fix and quietly mis-key a session instead.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
|
|
ENV_VAR = "WIKITOOL_SESSION_ID"
|
|
|
|
HARNESS_ENV_VARS: tuple[tuple[str, str], ...] = (
|
|
("CLAUDE_CODE_SESSION_ID", "claude-code"),
|
|
)
|
|
|
|
_UNSAFE = re.compile(r"[^A-Za-z0-9._-]+")
|
|
|
|
|
|
def _harness_session() -> tuple[str, str] | None:
|
|
"""The first harness variable that is actually set, as `(value, harness)`."""
|
|
for var, harness in HARNESS_ENV_VARS:
|
|
value = os.environ.get(var)
|
|
if value:
|
|
return value, harness
|
|
return None
|
|
|
|
|
|
def session_id() -> str:
|
|
explicit = os.environ.get(ENV_VAR)
|
|
if explicit:
|
|
return explicit
|
|
harness = _harness_session()
|
|
if harness:
|
|
return harness[0]
|
|
return str(os.getppid())
|
|
|
|
|
|
def session_id_source() -> str:
|
|
"""Where the id in `session_id()` came from - `ENV_VAR`, a harness
|
|
variable name (with the harness named alongside it), or the `getppid()`
|
|
fallback. `doctor`, `budget status` and the `wikitool` source's
|
|
`session.start` event all read this so a session - or a trace - can say
|
|
what it was keyed on, not just what the id happened to be."""
|
|
if os.environ.get(ENV_VAR):
|
|
return ENV_VAR
|
|
harness = _harness_session()
|
|
if harness:
|
|
_, name = harness
|
|
var = next(v for v, h in HARNESS_ENV_VARS if h == name)
|
|
return f"{var} ({name})"
|
|
return "getppid() fallback"
|
|
|
|
|
|
def session_slug(value: str | None = None) -> str:
|
|
"""A session id that is safe as a single directory name.
|
|
|
|
`ingest-large-tree.md` hands out ids like `<runkey>/u2`, so the separator
|
|
has to survive as a name rather than becoming a nested directory.
|
|
"""
|
|
return _UNSAFE.sub("__", value or session_id()).strip("_") or "unknown"
|