Skip to content

Commit 0884eda

Browse files
committed
chore: sync from source 0bb7e87
1 parent 30f8d88 commit 0884eda

4 files changed

Lines changed: 30 additions & 241 deletions

File tree

agents/claude-code/hooks/user_prompt_context_saver.py

Lines changed: 3 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"""
2121

2222
import asyncio
23-
import contextlib
2423
import ctypes
2524
import importlib.util
2625
import io
@@ -129,139 +128,6 @@ def _load_config_loader() -> ModuleType:
129128
}
130129

131130

132-
def _persist_user_prompt_context_id(
133-
session_id: str,
134-
context_ids: list[int],
135-
user_prompt: str,
136-
) -> None:
137-
"""Persist stored user-message context_id(s) and verbatim text to AEGIS runtime files.
138-
139-
Side-effect-only helper used by orchestrator_sequencing_enforcement.py to:
140-
1. Append the context_id to the per-session observed_report_ids JSON file
141-
(so the orchestrator's `USER REQUEST CONTEXT ID: [N]` references pass
142-
the prompt_id_reference_validity validator).
143-
2. Write the FIRST stored context_id as plain text to a per-session sidecar
144-
file (so the verbatim_relay_check rule can detect when a context_id is
145-
available).
146-
3. Write the verbatim user prompt text to a per-session sidecar file
147-
(`last_user_prompt_text_{session_id}`) so the verbatim_relay_check rule
148-
can perform Option D's text-fragment match without round-tripping
149-
through the context-server at hook time.
150-
151-
Reliability hardening: each attempt is wrapped in a retry loop with one
152-
retry attempt and a 50 ms backoff between attempts. On persistent failure
153-
(both attempts raise), the failure is logged via `log_always(level='ERROR')`
154-
-- never raised. The function's contract is silent-side-effect-only so the
155-
hook's existing reliability guarantee (never break Claude Code workflow)
156-
is preserved.
157-
158-
Args:
159-
session_id: Claude Code session_id (extracted from input_data).
160-
context_ids: List of stored context IDs (single entry for non-chunked,
161-
multiple for chunked storage).
162-
user_prompt: The verbatim user prompt text (for the text sidecar).
163-
"""
164-
if not context_ids:
165-
return
166-
167-
last_err: Exception | None = None
168-
for attempt in range(2): # 1 try + 1 retry
169-
try:
170-
runtime_dir = Path(os.path.expanduser('~/.claude/aegis/runtime'))
171-
runtime_dir.mkdir(parents=True, exist_ok=True)
172-
observed_ids_file = runtime_dir / f'observed_report_ids_{session_id}.json'
173-
sidecar_file = runtime_dir / f'last_user_prompt_context_id_{session_id}'
174-
text_sidecar_file = runtime_dir / f'last_user_prompt_text_{session_id}'
175-
176-
# Step 1: read existing observed-IDs list, merge new IDs, write atomically.
177-
existing_ids: set[int] = set()
178-
try:
179-
if observed_ids_file.exists():
180-
loaded = json.loads(observed_ids_file.read_text(encoding='utf-8'))
181-
if isinstance(loaded, dict):
182-
loaded_typed = cast(dict[str, Any], loaded)
183-
raw_ids_value = cast(list[Any] | None, loaded_typed.get('report_ids'))
184-
if isinstance(raw_ids_value, list):
185-
for item in raw_ids_value:
186-
if isinstance(item, int):
187-
existing_ids.add(item)
188-
elif isinstance(item, str) and item.isdigit():
189-
existing_ids.add(int(item))
190-
except Exception:
191-
existing_ids = set()
192-
merged = sorted(existing_ids | {int(cid) for cid in context_ids})
193-
194-
# Atomic write of observed-IDs file (tempfile + os.replace).
195-
fd1, tmp_obs_str = tempfile.mkstemp(
196-
prefix=observed_ids_file.name + '.',
197-
suffix='.tmp',
198-
dir=str(runtime_dir),
199-
)
200-
try:
201-
with os.fdopen(fd1, 'w', encoding='utf-8') as f:
202-
json.dump({'report_ids': merged}, f, ensure_ascii=False)
203-
f.flush()
204-
os.fsync(f.fileno())
205-
os.replace(tmp_obs_str, str(observed_ids_file))
206-
except Exception:
207-
with contextlib.suppress(Exception):
208-
Path(tmp_obs_str).unlink(missing_ok=True)
209-
raise
210-
211-
# Step 2: write the first context_id to the sidecar (plain text).
212-
fd2, tmp_side_str = tempfile.mkstemp(
213-
prefix=sidecar_file.name + '.',
214-
suffix='.tmp',
215-
dir=str(runtime_dir),
216-
)
217-
try:
218-
with os.fdopen(fd2, 'w', encoding='utf-8') as f:
219-
f.write(str(int(context_ids[0])))
220-
f.flush()
221-
os.fsync(f.fileno())
222-
os.replace(tmp_side_str, str(sidecar_file))
223-
except Exception:
224-
with contextlib.suppress(Exception):
225-
Path(tmp_side_str).unlink(missing_ok=True)
226-
raise
227-
228-
# Step 3: write the verbatim user prompt text to the per-session sidecar
229-
# so the verbatim_relay_check rule can perform Option D's text-fragment
230-
# match without round-tripping through the context-server at hook time.
231-
fd3, tmp_text_str = tempfile.mkstemp(
232-
prefix=text_sidecar_file.name + '.',
233-
suffix='.tmp',
234-
dir=str(runtime_dir),
235-
)
236-
try:
237-
with os.fdopen(fd3, 'w', encoding='utf-8') as f:
238-
f.write(user_prompt)
239-
f.flush()
240-
os.fsync(f.fileno())
241-
os.replace(tmp_text_str, str(text_sidecar_file))
242-
except Exception:
243-
with contextlib.suppress(Exception):
244-
Path(tmp_text_str).unlink(missing_ok=True)
245-
raise
246-
return # success on first or retry attempt
247-
except Exception as e:
248-
last_err = e
249-
if attempt == 0:
250-
time.sleep(0.05) # 50 ms backoff before single retry
251-
continue
252-
253-
# Persistent failure: log via log_always ERROR; never raise. Use the
254-
# existing diagnostic channel so the message reaches the hook log alongside
255-
# all other ERROR-level diagnostics in this module.
256-
with contextlib.suppress(Exception):
257-
log_always(
258-
f'Failed to persist user-message context_id(s) to AEGIS runtime after retry: '
259-
f'{context_ids}. Last error: '
260-
f'{type(last_err).__name__ if last_err else "Unknown"}: {last_err}',
261-
level='ERROR',
262-
)
263-
264-
265131
def _get_log_file() -> Path:
266132
"""
267133
Get log file location with multiple fallbacks and diagnostic reporting.
@@ -1854,49 +1720,10 @@ def main() -> None:
18541720
else:
18551721
log_always(f'SUCCESS: All {total_chunks} chunks stored successfully')
18561722

1857-
# Output chunk IDs via additionalContext for orchestrator reference.
1858-
#
1859-
# Extract per-chunk context_ids from the `results` array returned by
1860-
# `_store_chunks_single_connection` (the dict has key `results`, NOT
1861-
# `chunk_ids`). Each entry is the raw CallToolResult from
1862-
# `client.call_tool('store_context', ...)`. FastMCP exposes the
1863-
# context_id either as `result.structured_content['context_id']`
1864-
# (canonical) or directly on the dict shape after `.structured_content`
1865-
# extraction. Handle both shapes defensively, mirroring the
1866-
# extraction in `_store_single_context_async` (lines 762-767).
1723+
# Output chunk IDs via additionalContext for orchestrator reference
18671724
if config.get('output_context_id', True):
1868-
chunk_ids: list[int] = []
1869-
results_list = cast(list[Any], result.get('results', []))
1870-
for entry_any in results_list:
1871-
if isinstance(entry_any, dict) and 'error' in cast(dict[str, Any], entry_any):
1872-
continue
1873-
candidate: Any = None
1874-
structured: Any = getattr(cast(Any, entry_any), 'structured_content', None)
1875-
if isinstance(structured, dict):
1876-
candidate = cast(dict[str, Any], structured).get('context_id')
1877-
if candidate is None and isinstance(entry_any, dict):
1878-
entry_typed = cast(dict[str, Any], entry_any)
1879-
nested: Any = entry_typed.get('structured_content')
1880-
if isinstance(nested, dict):
1881-
candidate = cast(dict[str, Any], nested).get('context_id')
1882-
if candidate is None:
1883-
candidate = entry_typed.get('context_id')
1884-
if candidate is None:
1885-
continue
1886-
try:
1887-
chunk_ids.append(int(candidate))
1888-
except (TypeError, ValueError):
1889-
continue
1725+
chunk_ids = result.get('chunk_ids', [])
18901726
if chunk_ids:
1891-
# Persist FIRST so the sidecar / observed-IDs files are durable
1892-
# on disk before the orchestrator can see additionalContext
1893-
# referencing these IDs. Closes the persist/emit race window
1894-
# at source: downstream readers (validate_agent_invocation,
1895-
# orchestrator_sequencing_enforcement) can rely on the files
1896-
# being present whenever the upstream prompt mentions an ID.
1897-
session_id_value = str(input_data.get('session_id', '')) or 'unknown'
1898-
_persist_user_prompt_context_id(session_id_value, chunk_ids, str(prompt))
1899-
19001727
hook_output: dict[str, Any] = {
19011728
'hookSpecificOutput': {
19021729
'hookEventName': 'UserPromptSubmit',
@@ -1912,19 +1739,10 @@ def main() -> None:
19121739
else:
19131740
log_always('SUCCESS: Context stored successfully')
19141741

1915-
# Output context_id via additionalContext for orchestrator reference.
1916-
# Persist FIRST so the sidecar / observed-IDs files are durable on disk
1917-
# before the orchestrator can see additionalContext referencing the ID.
1742+
# Output context_id via additionalContext for orchestrator reference
19181743
if config.get('output_context_id', True):
19191744
context_id = result.get('context_id')
19201745
if context_id is not None:
1921-
session_id_value = str(input_data.get('session_id', '')) or 'unknown'
1922-
try:
1923-
single_id = int(context_id)
1924-
_persist_user_prompt_context_id(session_id_value, [single_id], str(prompt))
1925-
except (TypeError, ValueError):
1926-
pass
1927-
19281746
hook_output = {
19291747
'hookSpecificOutput': {
19301748
'hookEventName': 'UserPromptSubmit',

agents/claude-code/rules/context-server-integration.md

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44

55
When MCP Context Server tools are available (any `mcp__context-server__*` tool in your tools list), you MUST follow this rule. If no context-server tools are present, this rule is inactive.
66

7-
## Mandatory Skill Delegation
7+
## Core Operating Principles
88

9-
For ALL context-server operations (retrieval, search, storage, metadata, update/revision, scoped retrieval, references navigation, continuity, pre-compaction patterns), you MUST follow these skills as the authoritative source of truth:
10-
11-
- **Retrieval:** `context-retrieval-protocol` skill -- thread ID acquisition, project name derivation, retrieval sequences, hybrid/semantic/FTS search, scoped retrieval (`context_scope`), references navigation, revision context detection, worktree-aware queries, and continuity patterns.
12-
- **Preservation:** `context-preservation-protocol` skill -- storage patterns, metadata schema (including the "task subject vs execution tools" distinction for the `technologies` field), `store_context` vs `update_context` strategy, handoff reports, and continuity patterns.
13-
14-
If these skills are already loaded in your context (via your agent frontmatter `skills:` field or a slash-command invocation), treat them as active. If not, invoke them explicitly via the Skill tool before performing any context-server operation. **Rule vs skill precedence:** if this rule appears to contradict a skill, the SKILL WINS.
9+
For ALL context-server operations (retrieval, search, storage, metadata, update/revision, scoped retrieval, references navigation, continuity, pre-compaction patterns), apply these principles directly. Where your environment provides skills, tutorials, or other operational guidance, treat that guidance as the practical embodiment of these principles; this rule supplies the invariants those guides must respect.
1510

1611
## Environment-Specific Facts
1712

@@ -20,35 +15,36 @@ If these skills are already loaded in your context (via your agent frontmatter `
2015

2116
## User Message Authority
2217

23-
User messages are the authoritative source of truth and override orchestrator summaries, agent reports, and your own memory when conflicts arise. User messages are IMMUTABLE -- never update, rewrite, or delete them, even when they contain errors. For discrepancy-handling details, see the retrieval skill's orchestrator-verification section.
18+
User messages are the authoritative source of truth and override orchestrator summaries, agent reports, and your own memory when conflicts arise. User messages are IMMUTABLE -- never update, rewrite, or delete them, even when they contain errors. When you detect a discrepancy between an orchestrator's task and the user's stated requirements (retrieved verbatim from the context server), the user-message wording wins; the orchestrator's framing is corrected, not the user's words.
2419

25-
## User Message Relay Protocol (ID-First)
20+
## User Message Relay Protocol
2621

27-
When launching subagents (via the Task or Agent tool) whose work depends on the user request, you MUST relay the user message using one of two modes, chosen by a deterministic predicate on `context_id` availability. Message SIZE is IRRELEVANT to mode selection.
22+
When launching subagents (via the Task or Agent tool) whose work depends on the user request, you MUST pass the user message in one of two modes:
2823

29-
- **Default Mode -- REFERENCE (context_id only):** This is the ALWAYS-PREFERRED mode regardless of message size. Use it whenever the UserPromptSubmit hook has emitted a `context_id` for the current user message. The Reference Block contains EXACTLY this one line and nothing else (no retrieval instructions, no CRITICAL reminders, no size annotations, no format descriptors):
24+
- **Mode 1 -- INLINE (default):** For moderate messages (guidance: under approximately 2000 tokens / 40 lines), include the full verbatim text under a `USER REQUEST:` marker:
3025

3126
```text
32-
USER REQUEST CONTEXT ID: [context_id from hook]
27+
USER REQUEST: [verbatim user message]
3328
```
3429

35-
- **Fallback Mode -- INLINE (verbatim text):** Use ONLY when the `context_id` is unavailable -- the UserPromptSubmit hook did not emit one (hook failure or upstream error), or the context-server is unreachable at message-store time. Include the full verbatim text under a `USER REQUEST:` marker:
30+
- **Mode 2 -- REFERENCE (large messages):** Use a reference block with explicit retrieval instructions, passing the `context_id` emitted by the hook:
3631

3732
```text
38-
USER REQUEST: [verbatim user message text]
33+
USER REQUEST (large message -- retrieve from context-server):
34+
Context ID: [context_id from hook additionalContext]
35+
Retrieve the FULL user message using: get_context_by_ids([<context_id>])
36+
CRITICAL: You MUST retrieve and read the COMPLETE user message before starting work.
3937
```
4038

41-
**Prohibitions (both modes):** MUST NOT summarize, paraphrase, condense, compress, or select "relevant" portions; MUST NOT extract quotes, evidence, or excerpts; MUST NOT describe intent or problem in your own words; MUST NOT add domain, technology, or problem qualifiers. Relay the complete message (REFERENCE) or its complete verbatim text (INLINE). When a subagent receives a Reference Block, it resolves the pointer per the retrieval skill's Pattern 6 (User Request Resolution) before starting work.
42-
43-
**Image Path Relay (extension to BOTH modes):** Images attached via the terminal are NOT stored in the context-server; subagents can only analyze them by reading the absolute paths directly with the `Read` tool. Whenever the task involves visual analysis, the orchestrator MUST forward image paths verbatim: in Fallback (INLINE) mode the paths flow naturally with the verbatim text; in Default (REFERENCE) mode the orchestrator MUST append an explicit `IMAGE PATHS` block after the Reference Block, because image paths cannot be retrieved from the context-server. The orchestrator MUST NOT summarize, describe, interpret, or redact image contents or paths.
39+
**Prohibitions (both modes):** MUST NOT summarize, paraphrase, condense, compress, or select "relevant" portions; MUST NOT extract quotes, evidence, or excerpts; MUST NOT describe intent or problem in your own words; MUST NOT add domain, technology, or problem qualifiers. Pass the complete message. **Fallback:** if the context server is unavailable, use Mode 1 INLINE regardless of size. When a subagent receives a Mode 2 reference block, it MUST resolve the pointer FIRST -- retrieve the full verbatim user message via `get_context_by_ids([<context_id>])`, read it in full, and only then begin work. Acting on the reference pointer without retrieving the underlying message is a PROTOCOL VIOLATION.
4440

4541
## Subagent Context Requirements
4642

4743
When launching subagents via the Task or Agent tool, the task description MUST include:
4844

4945
1. **Thread ID** -- enforced by a PreToolUse blocker
5046
2. **Timezone / date context** -- enforced by a PreToolUse blocker
51-
3. **User original request** -- per the Relay Protocol above (Default REFERENCE pointer, or INLINE fallback when no `context_id`)
47+
3. **User original request** -- per the Relay Protocol above (Mode 1 or Mode 2)
5248
4. **Relevant context IDs** -- so the subagent can retrieve prior work via `get_context_by_ids`
5349

5450
Items 1 and 2 are hook-enforced: if missing, the tool call is blocked with guidance via stderr -- revise the task description and retry. Items 3 and 4 are your responsibility.
@@ -62,4 +58,4 @@ Before context compaction, preserve (priority-ordered):
6258
- **Current task state** -- what is active and what remains
6359
- **User decisions** -- explicit choices made during this session
6460

65-
Do NOT attempt to preserve full content already stored in the context server -- use context IDs for retrieval after compaction. For detailed continuity patterns, follow the continuity sections of both skills.
61+
Do NOT attempt to preserve full content already stored in the context server -- use context IDs for retrieval after compaction. After any context window reset or compaction event, re-retrieve the highest-priority items above via `get_context_by_ids` before continuing work.

0 commit comments

Comments
 (0)