aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ai/protocols.org9
-rwxr-xr-x.ai/scripts/session-context-path25
-rw-r--r--.ai/scripts/tests/session-context-path.bats40
-rw-r--r--.ai/workflows/startup.org7
-rw-r--r--.ai/workflows/wrap-it-up.org10
-rw-r--r--claude-templates/.ai/protocols.org9
-rwxr-xr-xclaude-templates/.ai/scripts/session-context-path25
-rw-r--r--claude-templates/.ai/scripts/tests/session-context-path.bats40
-rw-r--r--claude-templates/.ai/workflows/startup.org7
-rw-r--r--claude-templates/.ai/workflows/wrap-it-up.org10
-rw-r--r--todo.org4
11 files changed, 179 insertions, 7 deletions
diff --git a/.ai/protocols.org b/.ai/protocols.org
index 6e415e3..d5290aa 100644
--- a/.ai/protocols.org
+++ b/.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/.ai/scripts/session-context-path b/.ai/scripts/session-context-path
new file mode 100755
index 0000000..8cc56f6
--- /dev/null
+++ b/.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/.ai/scripts/tests/session-context-path.bats b/.ai/scripts/tests/session-context-path.bats
new file mode 100644
index 0000000..ea8937d
--- /dev/null
+++ b/.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/.ai/workflows/startup.org b/.ai/workflows/startup.org
index 9b95b17..3d5ac66 100644
--- a/.ai/workflows/startup.org
+++ b/.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/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org
index a55f475..7fc86e4 100644
--- a/.ai/workflows/wrap-it-up.org
+++ b/.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)
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)
diff --git a/todo.org b/todo.org
index 5b7c924..dbe3820 100644
--- a/todo.org
+++ b/todo.org
@@ -1197,11 +1197,13 @@ The original handoff also noted a related anomaly: even with =--delete=, two fil
Source: =inbox/2026-05-29-0832-from-jr-estate-investigate-startup-rsync-carried-dirty.org= (processed and deleted).
-** TODO [#B] Codex Phase 1 — AI_AGENT_ID + session-context.d/<id>.org :feature:
+** DONE [#B] Codex Phase 1 — AI_AGENT_ID + session-context.d/<id>.org :feature:
+CLOSED: [2026-05-30 Sat]
:PROPERTIES:
:CREATED: [2026-05-28 Thu]
:LAST_REVIEWED: 2026-05-28
:END:
+Shipped backward-compatibly. New =.ai/scripts/session-context-path= helper resolves the active path from =AI_AGENT_ID=: unset → the legacy =.ai/session-context.org= singleton (one-agent default unchanged, per the spec's compatibility rule), set → =.ai/session-context.d/<sanitized-id>.org=. startup.org's existence check and wrap-it-up.org's rename now resolve through the helper (with a singleton fallback for older checkouts); wrap folds the agent id into the archive name. protocols.org documents the rule. Verified: 5 bats cases + a two-agent simulation showing distinct paths per id. Larger runtime-neutral arc (runtimes/ manifests, launcher refactor) stays parked under the parent spec.
Lifted from the broader codex runtime spec ([[file:docs/design/2026-05-28-generic-agent-runtime-spec.org]]) as the immediate-correctness slice independent of the larger arc. The singleton =.ai/session-context.org= is unsafe under simultaneous agents — two LLMs running in the same project at the same time would overwrite each other's session state.