diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-30 21:48:13 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-30 21:48:13 -0500 |
| commit | 143feda0644d2289954b694f3ce4cee2fc74b808 (patch) | |
| tree | 539452f3e7a47b24b2e9a0dd9a98e1e9c67f30a3 /claude-templates | |
| parent | 9a1bea9bfc066312bf9743dc23c88f191a36cc16 (diff) | |
| download | rulesets-143feda0644d2289954b694f3ce4cee2fc74b808.tar.gz rulesets-143feda0644d2289954b694f3ce4cee2fc74b808.zip | |
feat(session-context): resolve the active path per AI_AGENT_ID
A single .ai/session-context.org races when two agents share a project: each agent's writes clobber the other's session log. I added .ai/scripts/session-context-path, which resolves the active path from AI_AGENT_ID: unset gives the legacy .ai/session-context.org singleton (so every existing one-agent session is unchanged), set gives .ai/session-context.d/<id>.org with the id sanitized to filename-safe characters. This is Codex's Phase 1 slice from the runtime-neutral spec: the race fix on its own, no broader refactor.
startup.org's existence check and wrap-it-up.org's rename now resolve through the helper, each with a singleton fallback so older checkouts that haven't synced the script still work. Wrap folds the agent id into the archive name so two agents wrapping in the same minute don't collide. protocols.org documents the rule. Verified with 5 bats cases and a two-agent simulation showing distinct paths per id.
Diffstat (limited to 'claude-templates')
| -rw-r--r-- | claude-templates/.ai/protocols.org | 9 | ||||
| -rwxr-xr-x | claude-templates/.ai/scripts/session-context-path | 25 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/session-context-path.bats | 40 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/startup.org | 7 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/wrap-it-up.org | 10 |
5 files changed, 88 insertions, 3 deletions
diff --git a/claude-templates/.ai/protocols.org b/claude-templates/.ai/protocols.org index 6e415e3..d5290aa 100644 --- a/claude-templates/.ai/protocols.org +++ b/claude-templates/.ai/protocols.org @@ -84,6 +84,15 @@ Mechanics live in =startup.org= Phase A.0. The rule lives here because it govern Location during session: =.ai/session-context.org= Location after wrap-up: =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= +*** Agent-scoped path (=AI_AGENT_ID=) + +When two agents share one project at the same time, a single =session-context.org= is a race — each agent's writes clobber the other's. The active path is therefore resolved per agent: + +- =AI_AGENT_ID= unset or empty (the normal one-agent-per-project case): =.ai/session-context.org=, exactly as before. +- =AI_AGENT_ID= set: =.ai/session-context.d/<id>.org= (id sanitized to filename-safe chars). Archived at wrap-up to =.ai/sessions/YYYY-MM-DD-HH-MM-<id>-<description>.org= so concurrent agents don't collide on the archive name either. + +Resolve the path with =.ai/scripts/session-context-path= rather than hardcoding =.ai/session-context.org=; it prints the right path for the current =AI_AGENT_ID=. Fall back to =.ai/session-context.org= if the script isn't present (older checkouts mid-sync). Everything below — the record/recovery purpose, the update triggers, the startup existence check, the wrap-up rename — operates on that resolved path. The prose says "session-context.org" as the default name; read it as "the resolved active path" when =AI_AGENT_ID= is set. + This file serves two purposes with one mechanism: 1. *Crash recovery* — if the session dies mid-work, the live file is all that's left. On 2026-01-22 a session crashed during a 20-minute design discussion and all context was lost because this file wasn't being updated. 2. *Session archive* — at wrap-up the file is renamed into =.ai/sessions/=, becoming the permanent record. No transcription to notes.org; the file IS the record. diff --git a/claude-templates/.ai/scripts/session-context-path b/claude-templates/.ai/scripts/session-context-path new file mode 100755 index 0000000..8cc56f6 --- /dev/null +++ b/claude-templates/.ai/scripts/session-context-path @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# session-context-path — print the active session-context path for this agent. +# +# Single-agent (AI_AGENT_ID unset or empty): .ai/session-context.org — the +# historical singleton path, unchanged, so one-agent-per-project sessions +# behave exactly as before (Codex spec's compatibility rule). +# +# Multi-agent (AI_AGENT_ID set): .ai/session-context.d/<id>.org, so two agents +# running in the same project at the same time keep separate session logs +# instead of clobbering the singleton. The id is sanitized to filename-safe +# characters so a stray value can't escape the .d/ directory. +# +# Workflows call this to resolve the path; both startup (existence check) and +# wrap-up (rename source) read/write through it. Callers should fall back to +# .ai/session-context.org if this script isn't present yet (older checkouts +# mid-sync). +set -euo pipefail + +id="${AI_AGENT_ID:-}" +if [ -n "$id" ]; then + safe=$(printf '%s' "$id" | tr -c 'A-Za-z0-9._-' '_') + printf '.ai/session-context.d/%s.org\n' "$safe" +else + printf '.ai/session-context.org\n' +fi diff --git a/claude-templates/.ai/scripts/tests/session-context-path.bats b/claude-templates/.ai/scripts/tests/session-context-path.bats new file mode 100644 index 0000000..ea8937d --- /dev/null +++ b/claude-templates/.ai/scripts/tests/session-context-path.bats @@ -0,0 +1,40 @@ +#!/usr/bin/env bats +# Tests for the session-context-path helper: resolve the active session-context +# path from AI_AGENT_ID, defaulting to the legacy singleton. + +setup() { + SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + SCP="$SCRIPT_DIR/session-context-path" +} + +@test "session-context-path: no AI_AGENT_ID gives the legacy singleton" { + run env -u AI_AGENT_ID "$SCP" + [ "$status" -eq 0 ] + [ "$output" = ".ai/session-context.org" ] +} + +@test "session-context-path: empty AI_AGENT_ID gives the legacy singleton" { + AI_AGENT_ID="" run "$SCP" + [ "$status" -eq 0 ] + [ "$output" = ".ai/session-context.org" ] +} + +@test "session-context-path: a set AI_AGENT_ID gives a per-agent file" { + AI_AGENT_ID="pearl.org-drill.claude.a83f" run "$SCP" + [ "$status" -eq 0 ] + [ "$output" = ".ai/session-context.d/pearl.org-drill.claude.a83f.org" ] +} + +@test "session-context-path: two distinct ids resolve to distinct files" { + a=$(AI_AGENT_ID="claude" "$SCP") + b=$(AI_AGENT_ID="local-qwen" "$SCP") + [ "$a" = ".ai/session-context.d/claude.org" ] + [ "$b" = ".ai/session-context.d/local-qwen.org" ] + [ "$a" != "$b" ] +} + +@test "session-context-path: unsafe id characters are sanitized" { + AI_AGENT_ID="a b/c" run "$SCP" + [ "$status" -eq 0 ] + [ "$output" = ".ai/session-context.d/a_b_c.org" ] +} diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org index 9b95b17..3d5ac66 100644 --- a/claude-templates/.ai/workflows/startup.org +++ b/claude-templates/.ai/workflows/startup.org @@ -94,7 +94,12 @@ Phase A's rsyncs depend on the rulesets refresh completing first. The project-re These calls have no dependencies on each other. Issue them all together in one message: 1. =date "+%A %Y-%m-%d %H:%M %Z"= — accurate timestamp. -2. Check whether =.ai/session-context.org= exists (e.g. =[ -e .ai/session-context.org ] && echo present || echo absent=). +2. Check whether the active session-context file exists. Resolve the =AI_AGENT_ID=-aware path first (see protocols.org "Agent-scoped path"), then test it — the fallback keeps older projects without the helper working: + + #+begin_src bash + sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org) + [ -e "$sc" ] && echo "present: $sc" || echo "absent: $sc" + #+end_src 3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync. #+begin_src bash diff --git a/claude-templates/.ai/workflows/wrap-it-up.org b/claude-templates/.ai/workflows/wrap-it-up.org index a55f475..7fc86e4 100644 --- a/claude-templates/.ai/workflows/wrap-it-up.org +++ b/claude-templates/.ai/workflows/wrap-it-up.org @@ -63,10 +63,16 @@ Get current time and rename: #+begin_src bash mkdir -p .ai/sessions now=$(date +%Y-%m-%d-%H-%M) -mv .ai/session-context.org .ai/sessions/${now}-DESCRIPTION.org +# Resolve the AI_AGENT_ID-aware source path (see protocols.org "Agent-scoped +# path"); fall back to the singleton if the helper isn't present. +sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org) +# Under multi-agent, fold the agent id into the archive name so two agents +# wrapping in the same minute don't collide. Single-agent: no segment. +idseg="${AI_AGENT_ID:+${AI_AGENT_ID}-}" +mv "$sc" ".ai/sessions/${now}-${idseg}DESCRIPTION.org" #+end_src -Replace =DESCRIPTION= with your picked slug. +Replace =DESCRIPTION= with your picked slug. (=AI_AGENT_ID= should be filename-safe; the recommended =host.project.runtime.shortid= shape already is.) ** Step 3: todo.org cleanup (hygiene + archive completed work) |
