eagle-mem 4.11.0 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/db/044_summary_capture_source.sql +12 -0
- package/docs/agent-compatibility/claude-code.md +9 -1
- package/docs/agent-compatibility/codex.md +7 -1
- package/docs/agent-compatibility/opencode.md +2 -1
- package/hooks/session-start.sh +8 -17
- package/hooks/stop.sh +23 -3
- package/hooks/user-prompt-submit.sh +6 -4
- package/lib/common.sh +14 -7
- package/lib/db-summaries.sh +70 -3
- package/lib/hooks.sh +29 -0
- package/package.json +1 -1
- package/scripts/compaction.sh +1 -1
- package/scripts/enrich-summary.sh +8 -2
- package/scripts/install.sh +5 -0
- package/scripts/session.sh +155 -18
- package/scripts/update.sh +3 -0
- package/tests/test_clean_session_capture.sh +105 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to the **Eagle Mem** project are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v4.12.0 Clean, Branded Session Capture
|
|
8
|
+
|
|
9
|
+
Session endings are now clean across every agent — no more raw `<eagle-summary>` XML blocks in the visible reply.
|
|
10
|
+
|
|
11
|
+
- **CLI-first capture**: Agents persist structured summaries by running `eagle-mem session save --session-id <id> ...` (a pre-approved, quiet Bash call), then end with a short human recap plus one branded line: `Eagle Mem | Session captured — N decisions, M gotchas`. The installer adds a `permissions.allow` entry so the capture runs without a prompt.
|
|
12
|
+
- **Extended `session save`**: New flags `--session-id`, `--completed`, `--investigated`, `--files-read`, `--files-modified`, `--affected-features`, `--verified-features`, `--regression-risks`. With `--session-id` the capture merges into the live session row instead of creating a standalone `manual-*` row, and no longer marks the session completed.
|
|
13
|
+
- **Capture-integrity fixes**: New `capture_source` column (`agent`/`hook`/`enrich`) with an atomic fill-only upsert. Agent-authored captures are authoritative — Stop-hook heuristics and background LLM enrichment now only fill empty gaps and can never clobber richer data (previously the winning-COALESCE upsert could overwrite it). Enrichment queueing is skipped once a session is agent-authored.
|
|
14
|
+
- **Cross-agent**: Claude Code, Codex, OpenCode, Grok, and Antigravity all capture through the same CLI. Instruction text updated in SessionStart/UserPromptSubmit (Claude + Codex), the installed CLAUDE.md/AGENTS.md sections, and `compaction.sh`.
|
|
15
|
+
- **Backward compatible**: The Stop hook still parses any `<eagle-summary>` block it finds; agents are simply no longer instructed to emit one.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
7
19
|
## v4.11.0 Agent Compatibility and Governance Surfaces
|
|
8
20
|
|
|
9
21
|
This feature release expands Eagle Mem from Claude/Codex memory hooks into a broader multi-agent governance substrate:
|
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Hooks fire automatically at different points in the agent lifecycle:
|
|
|
88
88
|
| **PreToolUse** | before Bash/shell, Read, Edit, Write, apply_patch | Surfaces guardrails and decisions before edits. Blocks release-boundary commands while feature verification is pending. Rewrites noisy commands through RTK when available. Detects redundant reads, nudges co-edit partners, detects stuck loops. |
|
|
89
89
|
| **UserPromptSubmit** | user sends a message | FTS5 search across past sessions and indexed code for relevant context |
|
|
90
90
|
| **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes, surfaces decision history and feature impacts on reads, stale memory warnings on edits |
|
|
91
|
-
| **Stop** | agent turn ends | Saves
|
|
91
|
+
| **Stop** | agent turn ends | Saves the session summary. Agent-authored captures from `eagle-mem session save` are authoritative; heuristic extraction only fills gaps and LLM enrichment runs later in the background so the agent lifecycle is not blocked. |
|
|
92
92
|
| **SessionEnd** | session closes | Re-syncs tasks, marks session completed |
|
|
93
93
|
|
|
94
94
|
Codex shell hooks are registered for `Bash`, `exec_command`, `shell_command`, and `unified_exec` tool names so release-boundary protection works across current Codex shell paths. OpenCode uses a global local plugin to normalize `chat.message`, `tool.execute.before`, `tool.execute.after`, `todo.updated`, `session.idle`, and compaction events into the same Eagle Mem hook payloads.
|
|
@@ -128,7 +128,7 @@ Run `eagle-mem compaction` anytime to check readiness.
|
|
|
128
128
|
|
|
129
129
|
Eagle Mem prevents Claude from repeating past mistakes:
|
|
130
130
|
|
|
131
|
-
- **Decision surfacing** — when you edit a file that has past decisions recorded (from
|
|
131
|
+
- **Decision surfacing** — when you edit a file that has past decisions recorded (from captured session summaries), PreToolUse reminds Claude not to revert without asking
|
|
132
132
|
- **Guardrails** — file-level rules (manual or curator-discovered) that fire before every Edit/Write
|
|
133
133
|
- **Feature verification** — tracks features with smoke tests and dependencies; current git diffs create fingerprinted pending verification records, and release-boundary commands such as `git push`, `gh pr create`, and package publish are blocked until the current fingerprint is verified or waived
|
|
134
134
|
- **Gotcha surfacing** — past surprises and gotchas are surfaced when editing related files
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Migration 044: Summary capture provenance
|
|
2
|
+
-- Tracks how each summary row was captured so later writers do not clobber
|
|
3
|
+
-- richer data. Values:
|
|
4
|
+
-- 'agent' — agent-authored via `eagle-mem session save` or a parsed
|
|
5
|
+
-- <eagle-summary> block (authoritative)
|
|
6
|
+
-- 'hook' — Stop-hook heuristic extraction (gap-fill only once 'agent')
|
|
7
|
+
-- 'enrich' — background LLM enrichment (gap-fill only once 'agent')
|
|
8
|
+
-- Empty default preserves existing rows. Not indexed by FTS.
|
|
9
|
+
|
|
10
|
+
ALTER TABLE summaries ADD COLUMN capture_source TEXT NOT NULL DEFAULT '';
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_summaries_capture_source ON summaries(capture_source);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Claude Code Compatibility
|
|
2
2
|
|
|
3
|
-
Last verified: 2026-06-
|
|
3
|
+
Last verified: 2026-06-10
|
|
4
4
|
|
|
5
5
|
## Official Sources
|
|
6
6
|
|
|
@@ -21,6 +21,14 @@ Last verified: 2026-06-02
|
|
|
21
21
|
- `SessionEnd` runs when a session terminates.
|
|
22
22
|
- The custom statusline is configured with `statusLine` in Claude settings; its command receives JSON on stdin and the first stdout line becomes the visible statusline text.
|
|
23
23
|
|
|
24
|
+
## Session Capture Behavior
|
|
25
|
+
|
|
26
|
+
- Eagle Mem no longer instructs Claude to print a raw `<eagle-summary>` block. `SessionStart` injects guidance to capture the session by running `eagle-mem session save --session-id <session_id> ...` (a quiet shell call), then to end the turn with human prose plus one branded line: `Eagle Mem | Session captured — N decisions, M gotchas`.
|
|
27
|
+
- The `session_id` injected into the instruction is the same id Claude passes to every hook, so the CLI capture merges into the live session row rather than creating a standalone `manual-*` row.
|
|
28
|
+
- The installer adds `permissions.allow: ["Bash(eagle-mem session save:*)"]` to Claude settings so the capture runs without a permission prompt. The instruction must use that exact command prefix (no leading path, no `cd &&`) for the permission to match.
|
|
29
|
+
- Agent-authored captures are authoritative (`capture_source = agent`). The `Stop` hook still parses any `<eagle-summary>` block for backward compatibility, but when an agent row already exists its heuristics only fill empty fields and background enrichment is skipped — neither can clobber agent data.
|
|
30
|
+
- `UserPromptSubmit` context-pressure nudges (≥20 / ≥30 turns) also point at `eagle-mem session save`, not the raw block.
|
|
31
|
+
|
|
24
32
|
## Eagle Mem Files Depending On This
|
|
25
33
|
|
|
26
34
|
- `lib/hooks.sh`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Codex Compatibility
|
|
2
2
|
|
|
3
|
-
Last verified: 2026-06-
|
|
3
|
+
Last verified: 2026-06-10
|
|
4
4
|
|
|
5
5
|
## Official Sources
|
|
6
6
|
|
|
@@ -22,6 +22,12 @@ Last verified: 2026-06-02
|
|
|
22
22
|
- Hook `timeout` is measured in seconds; `statusMessage` is optional.
|
|
23
23
|
- Codex memory files are generated state. Required team guidance belongs in `AGENTS.md` or checked-in documentation, not only in local memories.
|
|
24
24
|
|
|
25
|
+
## Session Capture Behavior
|
|
26
|
+
|
|
27
|
+
- Codex replies stay prose-only; Eagle Mem never asks Codex to print summary blocks, XML, or hook payloads. The `Stop` hook captures a summary from the Codex rollout transcript automatically.
|
|
28
|
+
- For a richer structured capture, Codex may run `eagle-mem session save --session-id <session_id> --agent codex ...` once at wrap-up (injected by `SessionStart` and the `AGENTS.md` section). This sets `capture_source = agent`, which is authoritative: later Stop-hook heuristics only fill empty fields and background enrichment is skipped, so the capture is never clobbered.
|
|
29
|
+
- Modified-file lists are most reliable when Codex passes `--files-modified` explicitly; the transcript heuristic is Claude-shaped and may not populate file lists from Codex rollout tool calls.
|
|
30
|
+
|
|
25
31
|
## Eagle Mem Files Depending On This
|
|
26
32
|
|
|
27
33
|
- `lib/codex-hooks.sh`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenCode Compatibility
|
|
2
2
|
|
|
3
|
-
Last verified: 2026-06-
|
|
3
|
+
Last verified: 2026-06-10
|
|
4
4
|
|
|
5
5
|
## Official Sources
|
|
6
6
|
|
|
@@ -37,6 +37,7 @@ Last verified: 2026-06-02
|
|
|
37
37
|
- `session.idle` maps to Eagle Mem `Stop` using the latest assistant text accumulated from message events.
|
|
38
38
|
- `session.deleted` maps to Eagle Mem `SessionEnd`.
|
|
39
39
|
- `experimental.session.compacting` maps to Eagle Mem compact recall by running `SessionStart` with `source=compact` and appending the returned context.
|
|
40
|
+
- Session capture follows the shared clean-capture flow: the agent may run `eagle-mem session save --session-id <id> ...` at wrap-up (sets `capture_source = agent`, authoritative) and keep replies prose-only. The `session.idle` → `Stop` path then only fills gaps and never clobbers an agent-authored row.
|
|
40
41
|
|
|
41
42
|
## Eagle Mem Files Depending On This
|
|
42
43
|
|
package/hooks/session-start.sh
CHANGED
|
@@ -625,33 +625,24 @@ fi
|
|
|
625
625
|
if [ "$agent" = "codex" ]; then
|
|
626
626
|
context+="
|
|
627
627
|
=== Eagle Mem: Active ===
|
|
628
|
-
Memory active for '$project'. Keep user-facing Codex replies clean: do not print Eagle Mem summary capture blocks, XML, JSON hook payloads, or internal templates unless the user explicitly asks. The Stop hook captures summaries from the transcript automatically.
|
|
628
|
+
Memory active for '$project'. Keep user-facing Codex replies clean: do not print Eagle Mem summary capture blocks, XML, JSON hook payloads, or internal templates unless the user explicitly asks. The Stop hook captures summaries from the transcript automatically. When you wrap up substantive work, you may persist a richer structured capture by running once: eagle-mem session save --session-id $session_id --agent codex --completed \"...\" --decisions \"choice — why\" --gotchas \"...\" --files-modified \"a.ts,b.ts\" (fill what applies). Keep the visible reply prose-only.
|
|
629
629
|
"
|
|
630
630
|
elif [ "$source_type" = "compact" ] || [ "$source_type" = "clear" ]; then
|
|
631
631
|
context+="
|
|
632
632
|
=== Eagle Mem: Active ===
|
|
633
|
-
Memory active. Attribute recalled context to Eagle Mem. Do not revert PostToolUse-surfaced decisions without asking.
|
|
633
|
+
Memory active. Attribute recalled context to Eagle Mem. Do not revert PostToolUse-surfaced decisions without asking. Before your final response, capture the session quietly via the Bash tool (pre-approved): eagle-mem session save --session-id $session_id --completed \"...\" --decisions \"choice — why\" --gotchas \"...\" (fill what applies, omit the rest). Then give a short human recap — NEVER print <eagle-summary> tags or raw field lists.
|
|
634
634
|
"
|
|
635
635
|
else
|
|
636
636
|
context+="
|
|
637
637
|
=== Eagle Mem: Active ===
|
|
638
638
|
Memory active for '$project'. Scan, index, prune, and self-learning run automatically — never ask the user to run these. Attribute recalled context: \"Eagle Mem recalls:\" Do not revert PostToolUse-surfaced decisions without user request. No raw secrets in summaries. If you contradict a loaded memory, update the memory file.
|
|
639
639
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
request
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
gotchas: [what surprised]
|
|
647
|
-
next_steps: [concrete actions]
|
|
648
|
-
key_files: [path — role]
|
|
649
|
-
files_read: [path, ...]
|
|
650
|
-
files_modified: [path, ...]
|
|
651
|
-
affected_features: [feature, ...]
|
|
652
|
-
verified_features: [feature, ...]
|
|
653
|
-
regression_risks: [risk, ...]
|
|
654
|
-
</eagle-summary>
|
|
640
|
+
Session capture is clean: NEVER print <eagle-summary> tags, XML, or raw field lists (request:/completed:/...) in your visible reply. When you wrap up substantive work (shipped a change, made decisions worth remembering, the user signals done, or Eagle Mem warns of context pressure), capture it by running this ONCE via the Bash tool — it is pre-approved and prints only a short confirmation. Fill the flags you have; omit the rest:
|
|
641
|
+
|
|
642
|
+
eagle-mem session save --session-id $session_id --request \"...\" --completed \"...\" --learned \"...\" --decisions \"choice — why; choice — why\" --gotchas \"...\" --next-steps \"...\" --key-files \"path — role\" --files-modified \"a.ts,b.ts\" --affected-features \"...\" --verified-features \"...\" --regression-risks \"...\"
|
|
643
|
+
|
|
644
|
+
Then end with a brief, human recap of the session in prose, followed by one line:
|
|
645
|
+
**Eagle Mem** | Session captured — N decisions, M gotchas
|
|
655
646
|
"
|
|
656
647
|
fi
|
|
657
648
|
|
package/hooks/stop.sh
CHANGED
|
@@ -396,10 +396,30 @@ if [ -n "$regression_notes" ]; then
|
|
|
396
396
|
fi
|
|
397
397
|
|
|
398
398
|
# ─── Write to database ─────────────────────────────────────
|
|
399
|
+
#
|
|
400
|
+
# Capture-source precedence:
|
|
401
|
+
# - A parsed <eagle-summary> block this turn is agent-authored → write 'agent'.
|
|
402
|
+
# - An existing 'agent' row (CLI `eagle-mem session save` or an earlier block)
|
|
403
|
+
# is authoritative → fill empty gaps only, never overwrite with heuristics.
|
|
404
|
+
# - Otherwise this is a heuristic capture → normal 'hook' write.
|
|
405
|
+
# agent_authored also gates background enrichment so it can't clobber later.
|
|
406
|
+
|
|
407
|
+
existing_source=$(eagle_summary_capture_source "$session_id" 2>/dev/null || true)
|
|
408
|
+
agent_authored=0
|
|
409
|
+
[ -n "$summary_block" ] && agent_authored=1
|
|
410
|
+
[ "$existing_source" = "agent" ] && agent_authored=1
|
|
399
411
|
|
|
400
412
|
if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
|
|
401
|
-
|
|
402
|
-
|
|
413
|
+
write_rc=0
|
|
414
|
+
if [ -n "$summary_block" ]; then
|
|
415
|
+
eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "agent" || write_rc=$?
|
|
416
|
+
elif [ "$existing_source" = "agent" ]; then
|
|
417
|
+
eagle_insert_summary_fill_only "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "hook" || write_rc=$?
|
|
418
|
+
else
|
|
419
|
+
eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "hook" || write_rc=$?
|
|
420
|
+
fi
|
|
421
|
+
if [ "$write_rc" -eq 0 ]; then
|
|
422
|
+
eagle_log "INFO" "Stop: summary saved for session=$session_id (prior_source=${existing_source:-none}, block=$([ -n "$summary_block" ] && echo yes || echo no))"
|
|
403
423
|
eagle_insert_event "$project" "$session_id" "$agent" "memory_created" "" "Stop" "ok" "$(jq -nc --arg source "summary" '{source:$source}')" >/dev/null 2>&1 || true
|
|
404
424
|
else
|
|
405
425
|
eagle_log "ERROR" "Stop: summary insert FAILED for session=$session_id — check DB constraints"
|
|
@@ -413,7 +433,7 @@ eagle_hook_observability_set_detail "$(jq -nc \
|
|
|
413
433
|
'{request_chars:($request | length), completed_chars:($completed | length), files_read:$files_read, files_modified:$files_modified}')"
|
|
414
434
|
eagle_hook_observability_complete 0
|
|
415
435
|
|
|
416
|
-
if [ "$defer_enrichment" -eq 1 ] && [ "${EAGLE_MEM_STOP_BACKGROUND_ENRICH:-1}" = "1" ] && [ -n "$text_content" ]; then
|
|
436
|
+
if [ "$defer_enrichment" -eq 1 ] && [ "${EAGLE_MEM_STOP_BACKGROUND_ENRICH:-1}" = "1" ] && [ -n "$text_content" ] && [ "$agent_authored" -eq 0 ]; then
|
|
417
437
|
mkdir -p "$EAGLE_MEM_DIR/tmp" 2>/dev/null || true
|
|
418
438
|
enrich_job=$(mktemp "$EAGLE_MEM_DIR/tmp/summary-enrich.XXXXXX.json" 2>/dev/null)
|
|
419
439
|
if [ -n "$enrich_job" ]; then
|
|
@@ -67,8 +67,9 @@ Eagle Mem context pressure: critical ($turn_count turns since compact)
|
|
|
67
67
|
else
|
|
68
68
|
context+="
|
|
69
69
|
=== Eagle Mem: Context Pressure Critical ($turn_count turns since compact) ===
|
|
70
|
-
IMMEDIATELY
|
|
71
|
-
|
|
70
|
+
IMMEDIATELY capture ALL work this session by running (pre-approved, quiet):
|
|
71
|
+
eagle-mem session save --session-id $session_id --completed \"...\" --decisions \"choice — why\" --gotchas \"...\" --learned \"...\" --next-steps \"...\" --key-files \"path — role\"
|
|
72
|
+
Then tell the user to run /compact NOW. Do NOT print <eagle-summary> tags or raw field lists.
|
|
72
73
|
"
|
|
73
74
|
fi
|
|
74
75
|
echo "$turn_count" > "$EAGLE_MEM_DIR/.context-pressure"
|
|
@@ -83,8 +84,9 @@ Eagle Mem context pressure: high ($turn_count turns since compact)
|
|
|
83
84
|
else
|
|
84
85
|
context+="
|
|
85
86
|
=== Eagle Mem: Context Pressure High ($turn_count turns since compact) ===
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
Capture your work before compaction by running (pre-approved, quiet):
|
|
88
|
+
eagle-mem session save --session-id $session_id --completed \"...\" --decisions \"choice — why\" --gotchas \"...\" --learned \"...\"
|
|
89
|
+
Then suggest the user run /compact. Do NOT print <eagle-summary> tags or raw field lists.
|
|
88
90
|
"
|
|
89
91
|
fi
|
|
90
92
|
echo "$turn_count" > "$EAGLE_MEM_DIR/.context-pressure"
|
package/lib/common.sh
CHANGED
|
@@ -1893,16 +1893,19 @@ The person reading this transcript should experience natural, thoughtful, human
|
|
|
1893
1893
|
|
|
1894
1894
|
The rich structured data still gets saved (so future sessions get excellent context and the anti-regression system keeps working), but it happens without making the transcript feel like you are reading a debug dump.
|
|
1895
1895
|
|
|
1896
|
-
**
|
|
1897
|
-
-
|
|
1898
|
-
-
|
|
1899
|
-
|
|
1896
|
+
**How to capture (clean, no raw blocks):**
|
|
1897
|
+
- When you wrap up substantive work, run this once via the shell — it is pre-approved and prints only a short confirmation. Fill the flags you have; omit the rest:
|
|
1898
|
+
`eagle-mem session save --session-id <id> --request "..." --completed "..." --learned "..." --decisions "choice — why; choice — why" --gotchas "..." --next-steps "..." --key-files "path — role" --files-modified "a.ts,b.ts" --affected-features "..." --verified-features "..." --regression-risks "..."`
|
|
1899
|
+
Your session id is injected by SessionStart; use it verbatim so the capture merges into the live session.
|
|
1900
|
+
- Then end with a short, human recap in prose, followed by one line: `**Eagle Mem** | Session captured — N decisions, M gotchas`.
|
|
1901
|
+
- NEVER print `<eagle-summary>` tags, XML, or raw field lists (`request:`/`completed:`/...) in the visible reply.
|
|
1902
|
+
- If you never run the CLI, the Stop hook still captures a summary automatically as a lower-fidelity fallback — so capture is never lost, it is just richer when you run the command.
|
|
1900
1903
|
|
|
1901
1904
|
**Why this rule exists:** Every single session ends with a summary. If it looks technical, the entire conversation history slowly becomes unpleasant to read. The memory layer should make the experience of working with agents *more* human, not less.
|
|
1902
1905
|
|
|
1903
1906
|
**How to apply:**
|
|
1904
1907
|
- Write the visible recap in clear, narrative prose first.
|
|
1905
|
-
- Persist the structured details via
|
|
1908
|
+
- Persist the structured details via the `session save` command — never as raw tags in the main reply.
|
|
1906
1909
|
- Keep durable task descriptions truthful and up to date; they often carry more value across compactions than any one summary.
|
|
1907
1910
|
- When Eagle Mem context appears, attribute it naturally.
|
|
1908
1911
|
- Protect secrets. Update the durable record if you change direction.
|
|
@@ -1916,8 +1919,11 @@ eagle_patch_claude_md() {
|
|
|
1916
1919
|
mkdir -p "$HOME/.claude"
|
|
1917
1920
|
|
|
1918
1921
|
if [ -f "$claude_md" ] && grep -qF "$marker" "$claude_md" 2>/dev/null; then
|
|
1919
|
-
#
|
|
1920
|
-
|
|
1922
|
+
# Rewrite when the section uses an outdated capture doctrine:
|
|
1923
|
+
# - the old pipe-separated <eagle-summary> template, or
|
|
1924
|
+
# - the superseded "collapsed <details>" recommendation (pre-CLI-first).
|
|
1925
|
+
if grep -qF 'request: \[what user asked\] | completed:' "$claude_md" 2>/dev/null \
|
|
1926
|
+
|| grep -qF 'collapsed `<details>` element' "$claude_md" 2>/dev/null; then
|
|
1921
1927
|
# Replace the outdated section: remove old, append new
|
|
1922
1928
|
local tmp_md
|
|
1923
1929
|
tmp_md=$(mktemp)
|
|
@@ -1965,6 +1971,7 @@ Eagle Mem hooks are active for Codex in this project. SessionStart and UserPromp
|
|
|
1965
1971
|
- For broad multi-agent work, YOU run `eagle-mem orchestrate`; do not ask the user to run these commands
|
|
1966
1972
|
- Codex does not currently expose a persistent custom statusline like Claude Code; if the user asks for Eagle Mem status, run `eagle-mem statusline`
|
|
1967
1973
|
- For important decisions, preferences, gotchas, or durable project facts, include them briefly in normal prose. Eagle Mem will extract them from the transcript.
|
|
1974
|
+
- To persist a richer structured capture without printing anything raw, run once at wrap-up: `eagle-mem session save --agent codex --completed "..." --decisions "choice — why" --gotchas "..." --files-modified "a.ts,b.ts"` (fill what applies)
|
|
1968
1975
|
- Do not revert Eagle Mem-surfaced decisions without asking the user
|
|
1969
1976
|
- If Eagle Mem reports pending feature verification, verify or waive it before push/PR/publish
|
|
1970
1977
|
- Never put raw secrets in summaries
|
package/lib/db-summaries.sh
CHANGED
|
@@ -20,9 +20,10 @@ eagle_insert_summary() {
|
|
|
20
20
|
local gotchas; gotchas=$(eagle_sql_escape "${12:-}")
|
|
21
21
|
local key_files; key_files=$(eagle_sql_escape "${13:-}")
|
|
22
22
|
local agent; agent=$(eagle_sql_escape "${14:-$(eagle_agent_source)}")
|
|
23
|
+
local capture_source; capture_source=$(eagle_sql_escape "${15:-}")
|
|
23
24
|
|
|
24
25
|
eagle_db_pipe <<SQL
|
|
25
|
-
INSERT INTO summaries (session_id, project, agent, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files)
|
|
26
|
+
INSERT INTO summaries (session_id, project, agent, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files, capture_source)
|
|
26
27
|
VALUES (
|
|
27
28
|
'$session_id',
|
|
28
29
|
'$project',
|
|
@@ -37,7 +38,8 @@ VALUES (
|
|
|
37
38
|
'$notes',
|
|
38
39
|
'$decisions',
|
|
39
40
|
'$gotchas',
|
|
40
|
-
'$key_files'
|
|
41
|
+
'$key_files',
|
|
42
|
+
'$capture_source'
|
|
41
43
|
)
|
|
42
44
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
43
45
|
project = excluded.project,
|
|
@@ -52,10 +54,75 @@ ON CONFLICT(session_id) DO UPDATE SET
|
|
|
52
54
|
notes = COALESCE(NULLIF(excluded.notes, ''), summaries.notes),
|
|
53
55
|
decisions = COALESCE(NULLIF(excluded.decisions, ''), summaries.decisions),
|
|
54
56
|
gotchas = COALESCE(NULLIF(excluded.gotchas, ''), summaries.gotchas),
|
|
55
|
-
key_files = COALESCE(NULLIF(excluded.key_files, ''), summaries.key_files)
|
|
57
|
+
key_files = COALESCE(NULLIF(excluded.key_files, ''), summaries.key_files),
|
|
58
|
+
capture_source = CASE
|
|
59
|
+
WHEN summaries.capture_source = 'agent' THEN 'agent'
|
|
60
|
+
ELSE COALESCE(NULLIF(excluded.capture_source, ''), summaries.capture_source)
|
|
61
|
+
END;
|
|
56
62
|
SQL
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
# Fill-only upsert: OLD value wins, only blank columns get filled. Atomic —
|
|
66
|
+
# no read-modify-write race. Use when a richer agent-authored row already
|
|
67
|
+
# exists and a later writer (Stop heuristics, background enrichment) should
|
|
68
|
+
# only top up missing fields, never overwrite. capture_source stays 'agent'.
|
|
69
|
+
eagle_insert_summary_fill_only() {
|
|
70
|
+
local session_id; session_id=$(eagle_sql_escape "$1")
|
|
71
|
+
local project; project=$(eagle_sql_escape "$2")
|
|
72
|
+
local request; request=$(eagle_sql_escape "$3")
|
|
73
|
+
local investigated; investigated=$(eagle_sql_escape "$4")
|
|
74
|
+
local learned; learned=$(eagle_sql_escape "$5")
|
|
75
|
+
local completed; completed=$(eagle_sql_escape "$6")
|
|
76
|
+
local next_steps; next_steps=$(eagle_sql_escape "$7")
|
|
77
|
+
local files_read; files_read=$(eagle_sql_escape "$8")
|
|
78
|
+
local files_modified; files_modified=$(eagle_sql_escape "$9")
|
|
79
|
+
local notes; notes=$(eagle_sql_escape "${10:-}")
|
|
80
|
+
local decisions; decisions=$(eagle_sql_escape "${11:-}")
|
|
81
|
+
local gotchas; gotchas=$(eagle_sql_escape "${12:-}")
|
|
82
|
+
local key_files; key_files=$(eagle_sql_escape "${13:-}")
|
|
83
|
+
local agent; agent=$(eagle_sql_escape "${14:-$(eagle_agent_source)}")
|
|
84
|
+
local capture_source; capture_source=$(eagle_sql_escape "${15:-}")
|
|
85
|
+
|
|
86
|
+
eagle_db_pipe <<SQL
|
|
87
|
+
INSERT INTO summaries (session_id, project, agent, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files, capture_source)
|
|
88
|
+
VALUES (
|
|
89
|
+
'$session_id',
|
|
90
|
+
'$project',
|
|
91
|
+
'$agent',
|
|
92
|
+
'$request',
|
|
93
|
+
'$investigated',
|
|
94
|
+
'$learned',
|
|
95
|
+
'$completed',
|
|
96
|
+
'$next_steps',
|
|
97
|
+
'$files_read',
|
|
98
|
+
'$files_modified',
|
|
99
|
+
'$notes',
|
|
100
|
+
'$decisions',
|
|
101
|
+
'$gotchas',
|
|
102
|
+
'$key_files',
|
|
103
|
+
'$capture_source'
|
|
104
|
+
)
|
|
105
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
106
|
+
request = COALESCE(NULLIF(summaries.request, ''), NULLIF(excluded.request, ''), ''),
|
|
107
|
+
investigated = COALESCE(NULLIF(summaries.investigated, ''), NULLIF(excluded.investigated, ''), ''),
|
|
108
|
+
learned = COALESCE(NULLIF(summaries.learned, ''), NULLIF(excluded.learned, ''), ''),
|
|
109
|
+
completed = COALESCE(NULLIF(summaries.completed, ''), NULLIF(excluded.completed, ''), ''),
|
|
110
|
+
next_steps = COALESCE(NULLIF(summaries.next_steps, ''), NULLIF(excluded.next_steps, ''), ''),
|
|
111
|
+
files_read = COALESCE(NULLIF(summaries.files_read, '[]'), NULLIF(excluded.files_read, '[]'), '[]'),
|
|
112
|
+
files_modified = COALESCE(NULLIF(summaries.files_modified, '[]'), NULLIF(excluded.files_modified, '[]'), '[]'),
|
|
113
|
+
notes = COALESCE(NULLIF(summaries.notes, ''), NULLIF(excluded.notes, ''), ''),
|
|
114
|
+
decisions = COALESCE(NULLIF(summaries.decisions, ''), NULLIF(excluded.decisions, ''), ''),
|
|
115
|
+
gotchas = COALESCE(NULLIF(summaries.gotchas, ''), NULLIF(excluded.gotchas, ''), ''),
|
|
116
|
+
key_files = COALESCE(NULLIF(summaries.key_files, ''), NULLIF(excluded.key_files, ''), '');
|
|
117
|
+
SQL
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# How a session's summary was captured: 'agent' | 'hook' | 'enrich' | ''
|
|
121
|
+
eagle_summary_capture_source() {
|
|
122
|
+
local session_id; session_id=$(eagle_sql_escape "$1")
|
|
123
|
+
eagle_db "SELECT capture_source FROM summaries WHERE session_id = '$session_id' LIMIT 1;"
|
|
124
|
+
}
|
|
125
|
+
|
|
59
126
|
eagle_get_recent_summaries() {
|
|
60
127
|
local project_scope="${1:-}"
|
|
61
128
|
local limit; limit=$(eagle_sql_int "${2:-5}")
|
package/lib/hooks.sh
CHANGED
|
@@ -31,6 +31,35 @@ eagle_clean_hook_entries() {
|
|
|
31
31
|
"$settings" > "$tmp" && mv "$tmp" "$settings"
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
# Add a permissions.allow rule to Claude settings.json so agent-issued capture
|
|
35
|
+
# commands (e.g. `eagle-mem session save`) run without a permission prompt.
|
|
36
|
+
# Idempotent and order-preserving: appends only when the rule is absent, and
|
|
37
|
+
# leaves existing permissions.deny/ask and allow entries untouched.
|
|
38
|
+
eagle_patch_permission_allow() {
|
|
39
|
+
local settings="$1"
|
|
40
|
+
local rule="$2"
|
|
41
|
+
[ -f "$settings" ] || return 0
|
|
42
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
43
|
+
|
|
44
|
+
if jq -e --arg r "$rule" '(.permissions.allow // []) | index($r) != null' "$settings" >/dev/null 2>&1; then
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
eagle_backup_file "$settings"
|
|
49
|
+
local tmp
|
|
50
|
+
tmp=$(mktemp)
|
|
51
|
+
if jq --arg r "$rule" '
|
|
52
|
+
.permissions = (.permissions // {})
|
|
53
|
+
| .permissions.allow = ((.permissions.allow // []) + [$r])
|
|
54
|
+
' "$settings" > "$tmp" 2>/dev/null; then
|
|
55
|
+
mv "$tmp" "$settings"
|
|
56
|
+
else
|
|
57
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
eagle_patch_hook() {
|
|
35
64
|
local settings="$1"
|
|
36
65
|
local event="$2"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eagle-mem",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.0",
|
|
4
4
|
"description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, OpenCode, Grok, and Google Antigravity",
|
|
5
5
|
"bin": {
|
|
6
6
|
"eagle-mem": "bin/eagle-mem"
|
package/scripts/compaction.sh
CHANGED
|
@@ -157,7 +157,7 @@ if [ "$readiness" = "strong" ]; then
|
|
|
157
157
|
elif [ "$readiness" = "moderate" ]; then
|
|
158
158
|
eagle_info "Compaction Survival: Moderate — add more durable tasks and summaries"
|
|
159
159
|
else
|
|
160
|
-
eagle_warn "Compaction Survival: Weak — start using durable tasks and
|
|
160
|
+
eagle_warn "Compaction Survival: Weak — start using durable tasks and 'eagle-mem session save' captures"
|
|
161
161
|
fi
|
|
162
162
|
|
|
163
163
|
echo ""
|
|
@@ -90,8 +90,14 @@ gotchas=$(extract_section "$enrich_result" "GOTCHAS" | eagle_redact)
|
|
|
90
90
|
key_files=$(extract_section "$enrich_result" "KEY_FILES" | eagle_redact)
|
|
91
91
|
|
|
92
92
|
if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ] || [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ]; then
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
existing_source=$(eagle_summary_capture_source "$session_id" 2>/dev/null || true)
|
|
94
|
+
if [ "$existing_source" = "agent" ]; then
|
|
95
|
+
# An agent-authored row exists — only fill gaps, never overwrite it.
|
|
96
|
+
eagle_insert_summary_fill_only "$session_id" "$project" "$request" "" "$learned" "$completed" "" "[]" "[]" "" "$decisions" "$gotchas" "$key_files" "$agent" "enrich"
|
|
97
|
+
else
|
|
98
|
+
eagle_insert_summary "$session_id" "$project" "$request" "" "$learned" "$completed" "" "[]" "[]" "" "$decisions" "$gotchas" "$key_files" "$agent" "enrich"
|
|
99
|
+
fi
|
|
100
|
+
eagle_log "INFO" "Summary enrichment saved for session=$session_id provider=$provider source=${existing_source:-none}"
|
|
95
101
|
fi
|
|
96
102
|
|
|
97
103
|
rm -f "$job_file" 2>/dev/null || true
|
package/scripts/install.sh
CHANGED
|
@@ -319,6 +319,11 @@ if [ "$claude_found" = true ]; then
|
|
|
319
319
|
eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash|Read|Edit|Write" \
|
|
320
320
|
"$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
|
|
321
321
|
"PreToolUse hook"
|
|
322
|
+
|
|
323
|
+
# Allow agent-issued session capture to run without a permission prompt
|
|
324
|
+
if eagle_patch_permission_allow "$SETTINGS" "Bash(eagle-mem session save:*)"; then
|
|
325
|
+
eagle_ok "Capture permission ${DIM}(eagle-mem session save)${RESET}"
|
|
326
|
+
fi
|
|
322
327
|
fi
|
|
323
328
|
else
|
|
324
329
|
eagle_info "Claude hooks skipped ${DIM}(Claude Code not detected)${RESET}"
|
package/scripts/session.sh
CHANGED
|
@@ -19,21 +19,30 @@ show_help() {
|
|
|
19
19
|
echo -e " eagle-mem session ${CYAN}save <text>${RESET}"
|
|
20
20
|
echo ""
|
|
21
21
|
echo -e " ${BOLD}Options for save:${RESET}"
|
|
22
|
-
echo -e " ${CYAN}--
|
|
22
|
+
echo -e " ${CYAN}--completed${RESET} <text> What was accomplished (alias: --summary)"
|
|
23
23
|
echo -e " ${CYAN}--request${RESET} <text> User request that caused the work"
|
|
24
|
+
echo -e " ${CYAN}--investigated${RESET} <text> What was explored or analyzed"
|
|
24
25
|
echo -e " ${CYAN}--learned${RESET} <text> Non-obvious discoveries"
|
|
25
26
|
echo -e " ${CYAN}--decisions${RESET} <text> Decisions and why"
|
|
26
27
|
echo -e " ${CYAN}--gotchas${RESET} <text> Surprises or pitfalls"
|
|
27
28
|
echo -e " ${CYAN}--next-steps${RESET} <text> Follow-up work"
|
|
28
|
-
echo -e " ${CYAN}--key-files${RESET} <text> Important files"
|
|
29
|
+
echo -e " ${CYAN}--key-files${RESET} <text> Important files (path — role)"
|
|
30
|
+
echo -e " ${CYAN}--files-read${RESET} <list> Comma-separated files read"
|
|
31
|
+
echo -e " ${CYAN}--files-modified${RESET} <list> Comma-separated files modified"
|
|
32
|
+
echo -e " ${CYAN}--affected-features${RESET} <text> Features touched (need re-verify)"
|
|
33
|
+
echo -e " ${CYAN}--verified-features${RESET} <text> Features verified this session"
|
|
34
|
+
echo -e " ${CYAN}--regression-risks${RESET} <text> Known risks introduced"
|
|
29
35
|
echo -e " ${CYAN}--notes${RESET} <text> Extra notes"
|
|
36
|
+
echo -e " ${CYAN}--session-id${RESET} <id> Live session id (merges into the active row;"
|
|
37
|
+
echo -e " omit for a standalone manual save)"
|
|
30
38
|
echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current git root)"
|
|
31
|
-
echo -e " ${CYAN}--agent${RESET} <name> Source
|
|
39
|
+
echo -e " ${CYAN}--agent${RESET} <name> Source: claude-code, codex, antigravity, opencode, grok"
|
|
32
40
|
echo -e " ${CYAN}--cwd${RESET} <path> Working directory for project detection"
|
|
33
41
|
echo -e " ${CYAN}--json${RESET} Output JSON"
|
|
34
42
|
echo ""
|
|
35
|
-
echo -e " ${DIM}
|
|
36
|
-
echo -e " ${DIM}
|
|
43
|
+
echo -e " ${DIM}Agents use this to capture a clean, branded session summary without printing${RESET}"
|
|
44
|
+
echo -e " ${DIM}raw blocks. Pass --session-id <id> to merge into the live session row. Stop${RESET}"
|
|
45
|
+
echo -e " ${DIM}hooks still capture automatically as a safety net when no save is made.${RESET}"
|
|
37
46
|
echo ""
|
|
38
47
|
}
|
|
39
48
|
|
|
@@ -49,24 +58,48 @@ json_string() {
|
|
|
49
58
|
jq -Rn --arg v "${1:-}" '$v'
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
# Comma-separated list → JSON array (matches hooks/stop.sh storage shape).
|
|
62
|
+
# Safe under set -e: empty input yields [] without a failing pipeline.
|
|
63
|
+
# Slurp the whole value (-s) so embedded newlines split into items rather than
|
|
64
|
+
# emitting one JSON array per line (which would corrupt the stored column).
|
|
65
|
+
csv_to_json_array() {
|
|
66
|
+
local raw="${1:-}"
|
|
67
|
+
[ -z "$raw" ] && { echo '[]'; return 0; }
|
|
68
|
+
printf '%s' "$raw" | jq -Rsc 'split("[,\n]"; "") | map(gsub("^\\s+|\\s+$";"")) | map(select(. != ""))'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Count items in a field separated by ';' or newlines (for the capture banner).
|
|
72
|
+
count_items() {
|
|
73
|
+
local text="${1:-}"
|
|
74
|
+
[ -z "$text" ] && { echo 0; return 0; }
|
|
75
|
+
printf '%s' "$text" | awk 'BEGIN{RS="[;\n]"} {gsub(/^[ \t]+|[ \t]+$/,""); if($0!="") n++} END{print n+0}'
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
save_session() {
|
|
53
79
|
local summary=""
|
|
54
|
-
local request="
|
|
80
|
+
local request=""
|
|
81
|
+
local investigated=""
|
|
55
82
|
local learned=""
|
|
56
83
|
local decisions=""
|
|
57
84
|
local gotchas=""
|
|
58
85
|
local next_steps=""
|
|
59
86
|
local key_files=""
|
|
87
|
+
local files_read_raw=""
|
|
88
|
+
local files_modified_raw=""
|
|
89
|
+
local affected_features=""
|
|
90
|
+
local verified_features=""
|
|
91
|
+
local regression_risks=""
|
|
60
92
|
local notes=""
|
|
61
93
|
local project=""
|
|
62
94
|
local cwd
|
|
63
95
|
cwd="$(pwd)"
|
|
64
96
|
local agent=""
|
|
97
|
+
local cli_session_id=""
|
|
65
98
|
local json_output=false
|
|
66
99
|
|
|
67
100
|
while [ $# -gt 0 ]; do
|
|
68
101
|
case "$1" in
|
|
69
|
-
--summary)
|
|
102
|
+
--summary|--completed)
|
|
70
103
|
require_value "$1" "${2:-}"
|
|
71
104
|
summary="$2"
|
|
72
105
|
shift 2
|
|
@@ -76,6 +109,11 @@ save_session() {
|
|
|
76
109
|
request="$2"
|
|
77
110
|
shift 2
|
|
78
111
|
;;
|
|
112
|
+
--investigated)
|
|
113
|
+
require_value "$1" "${2:-}"
|
|
114
|
+
investigated="$2"
|
|
115
|
+
shift 2
|
|
116
|
+
;;
|
|
79
117
|
--learned)
|
|
80
118
|
require_value "$1" "${2:-}"
|
|
81
119
|
learned="$2"
|
|
@@ -101,11 +139,41 @@ save_session() {
|
|
|
101
139
|
key_files="$2"
|
|
102
140
|
shift 2
|
|
103
141
|
;;
|
|
142
|
+
--files-read)
|
|
143
|
+
require_value "$1" "${2:-}"
|
|
144
|
+
files_read_raw="$2"
|
|
145
|
+
shift 2
|
|
146
|
+
;;
|
|
147
|
+
--files-modified)
|
|
148
|
+
require_value "$1" "${2:-}"
|
|
149
|
+
files_modified_raw="$2"
|
|
150
|
+
shift 2
|
|
151
|
+
;;
|
|
152
|
+
--affected-features)
|
|
153
|
+
require_value "$1" "${2:-}"
|
|
154
|
+
affected_features="$2"
|
|
155
|
+
shift 2
|
|
156
|
+
;;
|
|
157
|
+
--verified-features)
|
|
158
|
+
require_value "$1" "${2:-}"
|
|
159
|
+
verified_features="$2"
|
|
160
|
+
shift 2
|
|
161
|
+
;;
|
|
162
|
+
--regression-risks)
|
|
163
|
+
require_value "$1" "${2:-}"
|
|
164
|
+
regression_risks="$2"
|
|
165
|
+
shift 2
|
|
166
|
+
;;
|
|
104
167
|
--notes)
|
|
105
168
|
require_value "$1" "${2:-}"
|
|
106
169
|
notes="$2"
|
|
107
170
|
shift 2
|
|
108
171
|
;;
|
|
172
|
+
--session-id)
|
|
173
|
+
require_value "$1" "${2:-}"
|
|
174
|
+
cli_session_id="$2"
|
|
175
|
+
shift 2
|
|
176
|
+
;;
|
|
109
177
|
--project|-p)
|
|
110
178
|
require_value "$1" "${2:-}"
|
|
111
179
|
project="$2"
|
|
@@ -147,11 +215,25 @@ save_session() {
|
|
|
147
215
|
esac
|
|
148
216
|
done
|
|
149
217
|
|
|
150
|
-
if [ -z "$summary" ]
|
|
151
|
-
|
|
218
|
+
if [ -z "$summary" ] && [ -z "$learned" ] && [ -z "$decisions" ] && [ -z "$gotchas" ] \
|
|
219
|
+
&& [ -z "$next_steps" ] && [ -z "$key_files" ] && [ -z "$investigated" ] \
|
|
220
|
+
&& [ -z "$files_read_raw" ] && [ -z "$files_modified_raw" ] \
|
|
221
|
+
&& [ -z "$affected_features" ] && [ -z "$verified_features" ] && [ -z "$regression_risks" ]; then
|
|
222
|
+
eagle_err "Nothing to save. Pass --completed/--summary (or another field)."
|
|
152
223
|
exit 1
|
|
153
224
|
fi
|
|
154
225
|
|
|
226
|
+
if [ -n "$cli_session_id" ] && ! eagle_validate_session_id "$cli_session_id"; then
|
|
227
|
+
eagle_err "Invalid --session-id (allowed: letters, digits, '_', '-', max 128 chars)."
|
|
228
|
+
exit 1
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# For a live session, prefer its recorded project so a save issued from a
|
|
232
|
+
# subdirectory (whose cwd derives a different key) does not trigger an
|
|
233
|
+
# unintended project rekey of the active session.
|
|
234
|
+
if [ -z "$project" ] && [ -n "$cli_session_id" ] && [ -f "${EAGLE_MEM_DB:-}" ]; then
|
|
235
|
+
project=$(eagle_db "SELECT project FROM sessions WHERE id='$(eagle_sql_escape "$cli_session_id")' LIMIT 1;" 2>/dev/null || true)
|
|
236
|
+
fi
|
|
155
237
|
[ -z "$project" ] && project=$(eagle_project_from_cwd "$cwd")
|
|
156
238
|
if [ -z "$project" ]; then
|
|
157
239
|
eagle_err "Could not determine project. Re-run with --project <name>."
|
|
@@ -165,44 +247,99 @@ save_session() {
|
|
|
165
247
|
codex|openai-codex) agent="codex" ;;
|
|
166
248
|
claude|claude-code|cloud-code) agent="claude-code" ;;
|
|
167
249
|
antigravity*|google-antigravity*|google_antigravity*) agent="antigravity" ;;
|
|
250
|
+
opencode) agent="opencode" ;;
|
|
251
|
+
grok|grok-cli) agent="grok" ;;
|
|
168
252
|
*)
|
|
169
|
-
eagle_err "--agent must be codex, claude-code, or
|
|
253
|
+
eagle_err "--agent must be codex, claude-code, antigravity, opencode, or grok"
|
|
170
254
|
exit 1
|
|
171
255
|
;;
|
|
172
256
|
esac
|
|
173
257
|
fi
|
|
174
258
|
|
|
259
|
+
# Default request only for manual (non-session-id) saves; for a live
|
|
260
|
+
# session leave it empty so the Stop hook's real request is preserved.
|
|
261
|
+
if [ -z "$request" ] && [ -z "$cli_session_id" ]; then
|
|
262
|
+
request="Manual session save"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
local files_read files_modified
|
|
266
|
+
files_read=$(csv_to_json_array "$files_read_raw")
|
|
267
|
+
files_modified=$(csv_to_json_array "$files_modified_raw")
|
|
268
|
+
|
|
175
269
|
summary=$(printf '%s' "$summary" | eagle_redact)
|
|
176
270
|
request=$(printf '%s' "$request" | eagle_redact)
|
|
271
|
+
investigated=$(printf '%s' "$investigated" | eagle_redact)
|
|
177
272
|
learned=$(printf '%s' "$learned" | eagle_redact)
|
|
178
273
|
decisions=$(printf '%s' "$decisions" | eagle_redact)
|
|
179
274
|
gotchas=$(printf '%s' "$gotchas" | eagle_redact)
|
|
180
275
|
next_steps=$(printf '%s' "$next_steps" | eagle_redact)
|
|
181
276
|
key_files=$(printf '%s' "$key_files" | eagle_redact)
|
|
182
277
|
notes=$(printf '%s' "$notes" | eagle_redact)
|
|
278
|
+
affected_features=$(printf '%s' "$affected_features" | eagle_redact)
|
|
279
|
+
verified_features=$(printf '%s' "$verified_features" | eagle_redact)
|
|
280
|
+
regression_risks=$(printf '%s' "$regression_risks" | eagle_redact)
|
|
281
|
+
|
|
282
|
+
# Fold feature-tracking fields into notes (same shape as hooks/stop.sh)
|
|
283
|
+
local regression_notes=""
|
|
284
|
+
[ -n "$affected_features" ] && regression_notes+="affected_features: $affected_features"
|
|
285
|
+
if [ -n "$verified_features" ]; then
|
|
286
|
+
[ -n "$regression_notes" ] && regression_notes+="; "
|
|
287
|
+
regression_notes+="verified_features: $verified_features"
|
|
288
|
+
fi
|
|
289
|
+
if [ -n "$regression_risks" ]; then
|
|
290
|
+
[ -n "$regression_notes" ] && regression_notes+="; "
|
|
291
|
+
regression_notes+="regression_risks: $regression_risks"
|
|
292
|
+
fi
|
|
293
|
+
if [ -n "$regression_notes" ]; then
|
|
294
|
+
if [ -n "$notes" ]; then
|
|
295
|
+
notes="${notes}; ${regression_notes}"
|
|
296
|
+
else
|
|
297
|
+
notes="$regression_notes"
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
183
300
|
|
|
184
301
|
eagle_ensure_db
|
|
185
302
|
|
|
186
|
-
local
|
|
187
|
-
|
|
188
|
-
|
|
303
|
+
local session_id end_after_save=1
|
|
304
|
+
if [ -n "$cli_session_id" ]; then
|
|
305
|
+
# Live session: ensure the row exists and stays active. Empty source/cwd
|
|
306
|
+
# preserve whatever SessionStart already recorded; do NOT end the session.
|
|
307
|
+
session_id="$cli_session_id"
|
|
308
|
+
end_after_save=0
|
|
309
|
+
eagle_upsert_session "$session_id" "$project" "" "" "" "$agent"
|
|
310
|
+
else
|
|
311
|
+
local stamp
|
|
312
|
+
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
|
313
|
+
session_id="manual-${stamp}-$$-${RANDOM:-0}"
|
|
314
|
+
eagle_upsert_session "$session_id" "$project" "$cwd" "" "manual" "$agent"
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# capture_source=agent: agent-authored capture is authoritative and must not
|
|
318
|
+
# be clobbered by later Stop-hook heuristics or background enrichment.
|
|
319
|
+
eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$summary" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent" "agent"
|
|
320
|
+
|
|
321
|
+
[ "$end_after_save" = "1" ] && eagle_end_session "$session_id"
|
|
189
322
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
323
|
+
local n_dec n_got dec_word got_word
|
|
324
|
+
n_dec=$(count_items "$decisions")
|
|
325
|
+
n_got=$(count_items "$gotchas")
|
|
326
|
+
[ "$n_dec" = "1" ] && dec_word="decision" || dec_word="decisions"
|
|
327
|
+
[ "$n_got" = "1" ] && got_word="gotcha" || got_word="gotchas"
|
|
193
328
|
|
|
194
329
|
if [ "$json_output" = true ]; then
|
|
195
330
|
printf '{'
|
|
196
331
|
printf '"session_id":%s,' "$(json_string "$session_id")"
|
|
197
332
|
printf '"project":%s,' "$(json_string "$project")"
|
|
198
333
|
printf '"agent":%s,' "$(json_string "$agent")"
|
|
334
|
+
printf '"decisions":%s,' "$n_dec"
|
|
335
|
+
printf '"gotchas":%s,' "$n_got"
|
|
199
336
|
printf '"summary":%s' "$(json_string "$summary")"
|
|
200
337
|
printf '}\n'
|
|
201
338
|
else
|
|
202
|
-
|
|
339
|
+
printf ' %bEagle Mem%b | Session captured — %s %s, %s %s\n' \
|
|
340
|
+
"$CYAN" "$RESET" "$n_dec" "$dec_word" "$n_got" "$got_word"
|
|
203
341
|
eagle_kv "Project:" "$project"
|
|
204
342
|
eagle_kv "Source:" "$(eagle_agent_label "$agent")"
|
|
205
|
-
eagle_kv "Session:" "$session_id"
|
|
206
343
|
fi
|
|
207
344
|
}
|
|
208
345
|
|
package/scripts/update.sh
CHANGED
|
@@ -100,6 +100,9 @@ if [ "$claude_found" = true ] && [ -f "$SETTINGS" ] && command -v jq &>/dev/null
|
|
|
100
100
|
eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
|
|
101
101
|
eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash|Read|Edit|Write" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
|
|
102
102
|
|
|
103
|
+
# Allow agent-issued session capture to run without a permission prompt
|
|
104
|
+
eagle_patch_permission_allow "$SETTINGS" "Bash(eagle-mem session save:*)"
|
|
105
|
+
|
|
103
106
|
eagle_ok "Hooks registered"
|
|
104
107
|
fi
|
|
105
108
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Clean, branded session capture regression test
|
|
4
|
+
# Covers: CLI-first capture, clobber prevention, backward-compat
|
|
5
|
+
# <eagle-summary> override, and enrichment fill-only.
|
|
6
|
+
# ═══════════════════════════════════════════════════════════
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
tmp_dir=$(mktemp -d)
|
|
11
|
+
trap 'rm -rf "$tmp_dir"' EXIT
|
|
12
|
+
|
|
13
|
+
export EAGLE_MEM_DIR="$tmp_dir/em"
|
|
14
|
+
export EAGLE_AGENT_SOURCE="claude-code"
|
|
15
|
+
export EAGLE_MEM_DISABLE_HOOKS=0
|
|
16
|
+
# Pin the project so both the CLI save and the Stop hook resolve identically
|
|
17
|
+
# (the temp cwd lives under /var/folders, which Eagle Mem treats as ephemeral).
|
|
18
|
+
export EAGLE_MEM_PROJECT="test/clean-capture"
|
|
19
|
+
mkdir -p "$EAGLE_MEM_DIR"
|
|
20
|
+
|
|
21
|
+
pass=0; fail=0
|
|
22
|
+
ok() { echo " ok: $1"; pass=$((pass+1)); }
|
|
23
|
+
bad() { echo " FAIL: $1" >&2; fail=$((fail+1)); }
|
|
24
|
+
|
|
25
|
+
bash "$ROOT_DIR/db/migrate.sh" >/dev/null 2>&1
|
|
26
|
+
|
|
27
|
+
. "$ROOT_DIR/lib/common.sh"
|
|
28
|
+
. "$ROOT_DIR/lib/db.sh"
|
|
29
|
+
|
|
30
|
+
# A real cwd for the hook payload; project is pinned via EAGLE_MEM_PROJECT.
|
|
31
|
+
work="$tmp_dir/work"
|
|
32
|
+
mkdir -p "$work"
|
|
33
|
+
PROJECT="$EAGLE_MEM_PROJECT"
|
|
34
|
+
ok "pinned project: $PROJECT"
|
|
35
|
+
|
|
36
|
+
field() { eagle_db "SELECT COALESCE($2,'') FROM summaries WHERE session_id='$1';"; }
|
|
37
|
+
|
|
38
|
+
# ── Scenario A: CLI capture then a heuristic Stop must NOT clobber it ──
|
|
39
|
+
SID="clean-capture-aaa111"
|
|
40
|
+
eagle_upsert_session "$SID" "$PROJECT" "$work" "" "startup" "claude-code"
|
|
41
|
+
|
|
42
|
+
bash "$ROOT_DIR/scripts/session.sh" save --session-id "$SID" --project "$PROJECT" \
|
|
43
|
+
--completed "Rich agent completed" \
|
|
44
|
+
--decisions "Pin v1 — why safety; Use TOTP — why no SMTP" \
|
|
45
|
+
--gotchas "needs PUBLIC_URL" >/dev/null 2>&1
|
|
46
|
+
|
|
47
|
+
[ "$(field "$SID" capture_source)" = "agent" ] && ok "A: capture_source=agent after CLI save" || bad "A: capture_source not agent"
|
|
48
|
+
|
|
49
|
+
# Heuristic Stop: transcript has an Edit (fills files), NO <eagle-summary> block.
|
|
50
|
+
tA="$tmp_dir/transcriptA.jsonl"
|
|
51
|
+
{
|
|
52
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"text","text":"please harden the pipeline"}]}}'
|
|
53
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Edit","input":{"file_path":"'"$work"'/foo.sh"}}]}}'
|
|
54
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"Done. We hardened the pipeline this session."}]}}'
|
|
55
|
+
} > "$tA"
|
|
56
|
+
|
|
57
|
+
stopA=$(jq -nc --arg s "$SID" --arg c "$work" --arg t "$tA" \
|
|
58
|
+
'{session_id:$s, cwd:$c, transcript_path:$t}')
|
|
59
|
+
echo "$stopA" | EAGLE_MEM_STOP_BACKGROUND_ENRICH=0 bash "$ROOT_DIR/hooks/stop.sh" >/dev/null 2>&1
|
|
60
|
+
|
|
61
|
+
[ "$(field "$SID" completed)" = "Rich agent completed" ] && ok "A: completed NOT clobbered by heuristic" || bad "A: completed was clobbered -> '$(field "$SID" completed)'"
|
|
62
|
+
[ "$(field "$SID" capture_source)" = "agent" ] && ok "A: capture_source still agent after Stop" || bad "A: capture_source downgraded"
|
|
63
|
+
case "$(field "$SID" files_modified)" in *foo.sh*) ok "A: files_modified gap-filled by heuristic" ;; *) bad "A: files_modified not filled -> '$(field "$SID" files_modified)'" ;; esac
|
|
64
|
+
case "$(field "$SID" decisions)" in *"Pin v1"*) ok "A: decisions preserved" ;; *) bad "A: decisions lost" ;; esac
|
|
65
|
+
mrows=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE session_id LIKE 'manual-%';")
|
|
66
|
+
[ "$mrows" = "0" ] && ok "A: no stray manual-* row" || bad "A: $mrows manual-* rows created"
|
|
67
|
+
sstatus=$(eagle_db "SELECT status FROM sessions WHERE id='$SID';")
|
|
68
|
+
[ "$sstatus" = "active" ] && ok "A: live session still active (not ended)" || bad "A: session status=$sstatus"
|
|
69
|
+
|
|
70
|
+
# ── Scenario B: a <eagle-summary> block still overrides (backward compat) ──
|
|
71
|
+
SIDB="clean-capture-bbb222"
|
|
72
|
+
eagle_upsert_session "$SIDB" "$PROJECT" "$work" "" "startup" "claude-code"
|
|
73
|
+
tB="$tmp_dir/transcriptB.jsonl"
|
|
74
|
+
blocktext='Summary below.
|
|
75
|
+
|
|
76
|
+
<eagle-summary>
|
|
77
|
+
request: legacy block path
|
|
78
|
+
completed: shipped via block
|
|
79
|
+
decisions: keep parser — why backward compat
|
|
80
|
+
gotchas: none
|
|
81
|
+
</eagle-summary>'
|
|
82
|
+
{
|
|
83
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"text","text":"do the legacy thing"}]}}'
|
|
84
|
+
printf '%s\n' "$(jq -nc --arg t "$blocktext" '{type:"assistant",message:{content:[{type:"text",text:$t}]}}')"
|
|
85
|
+
} > "$tB"
|
|
86
|
+
stopB=$(jq -nc --arg s "$SIDB" --arg c "$work" --arg t "$tB" '{session_id:$s, cwd:$c, transcript_path:$t}')
|
|
87
|
+
echo "$stopB" | EAGLE_MEM_STOP_BACKGROUND_ENRICH=0 bash "$ROOT_DIR/hooks/stop.sh" >/dev/null 2>&1
|
|
88
|
+
|
|
89
|
+
[ "$(field "$SIDB" capture_source)" = "agent" ] && ok "B: block sets capture_source=agent" || bad "B: capture_source=$(field "$SIDB" capture_source)"
|
|
90
|
+
case "$(field "$SIDB" completed)" in *"shipped via block"*) ok "B: completed parsed from block" ;; *) bad "B: block completed not parsed" ;; esac
|
|
91
|
+
case "$(field "$SIDB" decisions)" in *"keep parser"*) ok "B: decisions parsed from block" ;; *) bad "B: block decisions not parsed" ;; esac
|
|
92
|
+
|
|
93
|
+
# ── Scenario C: enrichment must not overwrite an agent row ──
|
|
94
|
+
SIDC="clean-capture-ccc333"
|
|
95
|
+
eagle_upsert_session "$SIDC" "$PROJECT" "$work" "" "startup" "claude-code"
|
|
96
|
+
eagle_insert_summary "$SIDC" "$PROJECT" "req" "" "" "agent completed C" "" "[]" "[]" "" "agent decision C" "" "" "claude-code" "agent"
|
|
97
|
+
# fill-only path used by enrich when source=agent: simulate enrich trying to overwrite
|
|
98
|
+
eagle_insert_summary_fill_only "$SIDC" "$PROJECT" "" "" "enriched learned" "ENRICH OVERWRITE" "" "[]" "[]" "" "ENRICH DECISION" "" "" "claude-code" "enrich"
|
|
99
|
+
[ "$(field "$SIDC" completed)" = "agent completed C" ] && ok "C: enrich fill-only did NOT overwrite completed" || bad "C: enrich overwrote -> '$(field "$SIDC" completed)'"
|
|
100
|
+
case "$(field "$SIDC" learned)" in *"enriched learned"*) ok "C: enrich filled empty learned gap" ;; *) bad "C: enrich did not fill empty learned" ;; esac
|
|
101
|
+
[ "$(field "$SIDC" capture_source)" = "agent" ] && ok "C: capture_source stays agent through enrich" || bad "C: capture_source changed"
|
|
102
|
+
|
|
103
|
+
echo ""
|
|
104
|
+
echo "test_clean_session_capture: $pass passed, $fail failed"
|
|
105
|
+
[ "$fail" -eq 0 ]
|