-
Notifications
You must be signed in to change notification settings - Fork 2
bench/phase4: add --api-base-url and reasoning_content fallback #314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4a4f7af
9a70ca8
750769c
0961f93
184e4df
5897f25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,18 +72,47 @@ def get_key(provider): | |
| "~/.config/ghost/config.yaml") | ||
|
|
||
|
|
||
| def get_key_openai_compat(): | ||
| """Get API key for OpenAI-compatible providers (OpenCode Go, etc.).""" | ||
| for var in ("OPENCODE_API_KEY", "ZEN_API_KEY", "OPENAI_API_KEY"): | ||
| k = os.environ.get(var) | ||
| if k: | ||
| return k | ||
| sys.exit("error: no API key found; set OPENCODE_API_KEY, ZEN_API_KEY, " | ||
| "or OPENAI_API_KEY") | ||
|
|
||
|
|
||
| # -------------------------------------------------------------------------- | ||
| # HTTP with retry (stdlib only; no openai/anthropic SDK dependency) | ||
| # -------------------------------------------------------------------------- | ||
| def _post(url, headers, body, max_retries=6): | ||
| def _post(url, headers, body, max_retries=30): | ||
| data = json.dumps(body).encode() | ||
| for attempt in range(max_retries): | ||
| req = urllib.request.Request(url, data=data, headers=headers, method="POST") | ||
| hdrs = {**headers, "User-Agent": "ghost-phase4-bench/1.0"} | ||
| req = urllib.request.Request(url, data=data, headers=hdrs, method="POST") | ||
| try: | ||
| with urllib.request.urlopen(req, timeout=300) as resp: | ||
| return json.loads(resp.read()) | ||
| except urllib.error.HTTPError as e: | ||
| status = e.code | ||
| if status == 429 and attempt < max_retries - 1: | ||
| try: | ||
| detail = e.read().decode() | ||
| except Exception: | ||
| detail = "" | ||
| # Parse GoUsageLimitError "Resets in Xmin" | ||
| import re as _re | ||
| m = _re.search(r"Resets in (\d+)min", detail) | ||
| if m: | ||
| wait = int(m.group(1)) * 60 | ||
| sys.stderr.write(f" rate limit: resets in {m.group(1)}min, " | ||
| f"sleeping {wait}s ({attempt + 1}/{max_retries})\n") | ||
| else: | ||
| wait = min(2 ** attempt, 30) | ||
| sys.stderr.write(f" http 429, retry in {wait}s " | ||
| f"({attempt + 1}/{max_retries})\n") | ||
| time.sleep(wait) | ||
| continue | ||
| if status in RETRY_STATUS and attempt < max_retries - 1: | ||
| wait = min(2 ** attempt, 30) | ||
| sys.stderr.write(f" http {status}, retry in {wait}s " | ||
|
|
@@ -107,14 +136,17 @@ def _post(url, headers, body, max_retries=6): | |
| raise RuntimeError("exhausted retries") | ||
|
|
||
|
|
||
| def chat(provider, model, key, prompt, max_tokens): | ||
| def chat(provider, model, key, prompt, max_tokens, api_base_url=None): | ||
| """Single-user-message chat completion, temperature 0. Returns text.""" | ||
| if provider == "openai": | ||
| body = {"model": model, "temperature": 0, "max_tokens": max_tokens, "n": 1, | ||
| "messages": [{"role": "user", "content": prompt}]} | ||
| headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"} | ||
| out = _post("https://api.openai.com/v1/chat/completions", headers, body) | ||
| return out["choices"][0]["message"]["content"] | ||
| base = (api_base_url or "https://api.openai.com").rstrip("/") | ||
| out = _post(f"{base}/v1/chat/completions", headers, body) | ||
|
Comment on lines
+145
to
+146
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target file ---'
cat -n bench/longmemeval/phase4/phase4_run.py | sed -n '1,230p'
printf '%s\n' '--- related URL, provider, retry, and CLI references ---'
rg -n -S --glob '!*.lock' 'api_base_url|api-key|Authorization|Retry-After|retry|provider|anthropic|https?://' bench/longmemeval/phase4 README* bench 2>/dev/null | sed -n '1,260p'
printf '%s\n' '--- repository diff summary ---'
git diff --statRepository: wcatz/ghost Length of output: 22252 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- phase4 README ---'
cat -n bench/longmemeval/phase4/README.md | sed -n '45,125p'
printf '%s\n' '--- remaining driver and CLI ---'
cat -n bench/longmemeval/phase4/phase4_run.py | sed -n '223,325p'
printf '%s\n' '--- URL handling and endpoint tests/configuration ---'
rg -n -S --glob '*.py' --glob '*.md' --glob '*.yaml' --glob '*.yml' \
'api_base_url|api-base-url|OPENCODE_API_KEY|ZEN_API_KEY|opencode.ai|localhost|127\.0\.0\.1|urlparse|urlsplit' .
printf '%s\n' '--- deterministic urllib URL behavior ---'
python3 - <<'PY'
from urllib.parse import urlsplit
from urllib.request import Request
for value in [
"https://api.example.test",
"http://api.example.test",
"http://127.0.0.1:8080",
"file:///tmp/receiver",
"ftp://api.example.test",
"//api.example.test",
"not-a-url",
]:
base = value.rstrip("/")
url = f"{base}/v1/chat/completions"
try:
req = Request(
url,
data=b'{"test":true}',
headers={"Authorization": "Bearer SECRET"},
method="POST",
)
print(value, "=>", req.full_url, "scheme=", urlsplit(req.full_url).scheme,
"host=", urlsplit(req.full_url).hostname,
"auth_header=", req.get_header("Authorization"))
except Exception as exc:
print(value, "=>", type(exc).__name__, str(exc))
PYRepository: wcatz/ghost Length of output: 12232 Reject non-HTTPS 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| msg = out["choices"][0]["message"] | ||
| # DeepSeek puts short answers in reasoning_content when max_tokens is tight | ||
| return msg.get("content") or msg.get("reasoning_content", "") | ||
| # anthropic | ||
| body = {"model": model, "temperature": 0, "max_tokens": max_tokens, | ||
| "messages": [{"role": "user", "content": prompt}]} | ||
|
|
@@ -160,7 +192,10 @@ def cmd_generate(args): | |
| tok = tiktoken.get_encoding("o200k_base") | ||
| max_ret = args.model_max_length - GEN_LENGTH - RESERVE | ||
|
|
||
| key = get_key(args.provider) | ||
| if args.api_base_url: | ||
| key = get_key_openai_compat() | ||
| else: | ||
| key = get_key(args.provider) | ||
|
Comment on lines
+195
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Reject incompatible provider and endpoint combinations. The CLI accepts Reject this combination during argument validation. Alternatively, implement a separate Anthropic-compatible custom endpoint path. Also applies to: 228-231, 303-305 🧰 Tools🪛 ast-grep (0.45.1)[warning] 198-198: File path is request-/variable-derived; validate and normalize to prevent path traversal. (open-filename-from-request) 🤖 Prompt for AI Agents |
||
| data = json.load(open(args.dataset)) | ||
| done = load_done(args.out) | ||
| if done: | ||
|
|
@@ -175,7 +210,8 @@ def cmd_generate(args): | |
| prompt = prepare_prompt( | ||
| entry, args.retriever_type, args.topk_context, args.useronly, | ||
| args.history_format, args.cot, tok, "openai", max_ret, "none") | ||
| answer = chat(args.provider, args.model, key, prompt, GEN_LENGTH).strip() | ||
| answer = chat(args.provider, args.model, key, prompt, GEN_LENGTH, | ||
| api_base_url=args.api_base_url).strip() | ||
| fout.write(json.dumps({"question_id": qid, "hypothesis": answer}) + "\n") | ||
| fout.flush() | ||
| n_done += 1 | ||
|
|
@@ -189,7 +225,10 @@ def cmd_generate(args): | |
| # -------------------------------------------------------------------------- | ||
| def cmd_judge(args): | ||
| _, get_anscheck_prompt = import_official(args.longmemeval_src) | ||
| key = get_key(args.provider) | ||
| if args.api_base_url: | ||
| key = get_key_openai_compat() | ||
| else: | ||
| key = get_key(args.provider) | ||
|
|
||
| meta = {e["question_id"]: e for e in json.load(open(args.dataset))} | ||
| out_path = args.judged or (args.hyp + f".eval-results-{args.model}") | ||
|
|
@@ -209,7 +248,8 @@ def cmd_judge(args): | |
| prompt = get_anscheck_prompt( | ||
| e["question_type"], e["question"], e["answer"], h["hypothesis"], | ||
| abstention=abstention) | ||
| resp = chat(args.provider, args.model, key, prompt, 10) | ||
| resp = chat(args.provider, args.model, key, prompt, 50, | ||
| api_base_url=args.api_base_url) | ||
|
Comment on lines
+251
to
+252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Preserve the official judge budget for standard providers. This call applies Use 50 only for the compatible DeepSeek mode, or add an explicit opt-in. Keep 10 as the default for comparable benchmark results. 🤖 Prompt for AI Agents |
||
| label = "yes" in resp.lower() | ||
| fout.write(json.dumps({ | ||
| "question_id": qid, "question_type": e["question_type"], | ||
|
|
@@ -260,6 +300,9 @@ def add_common(p): | |
| help="e.g. gpt-4o-2024-08-06 | claude-sonnet-5 | claude-opus-4-8") | ||
| p.add_argument("--longmemeval-src", | ||
| help="LongMemEval repo src/ dir (or set $LONGMEMEVAL_SRC)") | ||
| p.add_argument("--api-base-url", | ||
| help="Override API base URL for OpenAI-compatible providers " | ||
| "(e.g. https://opencode.ai/zen/go)") | ||
|
|
||
| g = sub.add_parser("generate", help="produce hypotheses JSONL") | ||
| add_common(g) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,243 @@ | ||||
| # Autonomous memory creation: in-session transcript capture | ||||
|
|
||||
| **Status:** Design approved (2026-08-17). Ready for implementation planning. | ||||
| **Author:** Wayne (wcatz) | ||||
| **Builds on:** #297 "feat: autonomous memory lifecycle — auto reflect, supersede, and resolve" | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 1. Problem | ||||
|
|
||||
| Ghost has a fully working memory *maintenance* story (reflect/resolve/supersede) | ||||
| and an explicit memory *creation* path (`ghost_memory_save` etc.), but **creation is | ||||
| entirely on the assistant to remember**. Nothing creates memories on its own. The | ||||
| stop hook only *nudges* — it scans the transcript, counts saves, and if a tool-using | ||||
| turn ends with zero saves it blocks once with "you used tools but saved nothing". | ||||
| The nudge relies on the model deciding *what* to save, and it can't distinguish | ||||
| "nothing worth saving" from "five important discoveries the model forgot". | ||||
|
|
||||
| Three concrete gaps: | ||||
|
|
||||
| 1. **No autonomous creation.** Memories appear only when the model explicitly calls a | ||||
| save tool. A long session that never saves loses everything it learned. | ||||
| 2. **Detail is lost at compaction/session-end.** There is no "save before you forget" | ||||
| point. Compaction summarizes context away; session end discards the transcript. | ||||
| 3. **Lifecycle runs on the wrong event.** `spawnResolveIfConfigured` and | ||||
| `spawnSupersedeIfConfigured` live in the `Stop` hook, which fires **every turn** — | ||||
| their full prologue (config load → `sql.Open` → `ResolveProject` → PID check) runs | ||||
| per turn when enabled. That work is semantically session-scoped and belongs on the | ||||
| once-per-session `SessionEnd` event. | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 2. Core concept: the session's own model reads its own transcript | ||||
|
|
||||
| The pivotal constraint, discovered during research: **MCP sampling only sees what the | ||||
| server sends.** `ai.SamplingProvider.Classify` passes exactly `systemPrompt + | ||||
| userContent` (and caps `MaxTokens: 16`); the sampled model has no access to the | ||||
| conversation context. Therefore autonomous creation must hand the raw material to the | ||||
| sampling call itself — and that raw material is the **transcript**, which the hook | ||||
| already knows how to locate (`transcript_path` arrives on every hook's stdin). | ||||
|
|
||||
| The design adds `ghost_capture`: an MCP tool that reads a bounded slice of the current | ||||
| session's transcript and asks the *calling session's own model* (via MCP sampling, zero | ||||
| Anthropic credits) to extract memories from it. The tool supplies the transcript slice, | ||||
| dedup context, and the write gate; the sampling model does the extraction. | ||||
|
|
||||
| **Trigger** is a nudge from the `Stop` hook (which supports `additionalContext`, a | ||||
| non-error guidance that continues the turn). Because `Stop` fires every turn, capture | ||||
| runs *as discoveries surface* — by the time compaction or session end arrives, detail is | ||||
| already in the DB. This is the direct answer to the "pre-compaction vs end-of-session" | ||||
| question: **neither** — capture per-turn so detail is never at risk. | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 3. Mechanism | ||||
|
|
||||
| ### 3.1 `ghost_capture` MCP tool | ||||
|
|
||||
| Signature: `ghost_capture(project_id, transcript_path?)`. `transcript_path` is optional; | ||||
| when omitted the tool reads the pending marker (see §3.3) for the latest session. | ||||
|
|
||||
| Flow: | ||||
|
|
||||
| 1. **Locate the transcript.** Prefer the explicit `transcript_path` arg; otherwise read | ||||
| the pending marker (`<dataDir>/capture-pending.json`). No marker and no arg → return a | ||||
| short "nothing pending" message, not an error. | ||||
| 2. **Resume from the capture cursor.** A per-project state file | ||||
| (`<dataDir>/capture-state-<projectID>.json`) records the last-captured transcript | ||||
| path and byte offset. Same path → stream from the offset (incremental); different | ||||
| path (new session) → stream from 0. | ||||
|
Comment on lines
+59
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
doc='docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md'
printf '%s\n' '--- design sections ---'
sed -n '45,90p' "$doc"
sed -n '145,185p' "$doc"
printf '%s\n' '--- relevant repository files ---'
rg -n --glob '!node_modules' --glob '!dist' \
'capture-pending|capture-state|ghost_capture|project_id|transcript_path|SessionEnd|pending marker' .
printf '%s\n' '--- memory store contract ---'
store=$(fd -t f 'store.go' . | head -n 1)
printf 'store=%s\n' "$store"
sed -n '530,580p' "$store"Repository: wcatz/ghost Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
doc='docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md'
printf '%s\n' '--- design security and lifecycle details ---'
sed -n '1,45p' "$doc"
sed -n '90,140p' "$doc"
sed -n '140,185p' "$doc"
printf '%s\n' '--- store implementation and project resolution ---'
fd -t f -p 'internal/memory' | sort
rg -n -A35 -B8 'func .*Create|func .*EnsureProject|func .*ResolveProject|resolveProjectID|Session.*Project|Session.*Context' internal/memory internal/mcpserver internal/mcpinit
printf '%s\n' '--- session request/context types and transcript handling ---'
rg -n -A25 -B15 'req\.Session|SessionStart|session_id|TranscriptPath|transcript_path|cwd' internal/mcpserver internal/mcpinitRepository: wcatz/ghost Length of output: 50368 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- MemoryStore.Create and project resolution ---'
rg -n -A45 -B10 \
'^func \(s \*Store\) Create|^func \(s \*Store\) EnsureProject|^func \(s \*Store\) ResolveProject|^func \(s \*Store\) resolve' \
internal/memory/store.go
printf '%s\n' '--- project ID generation and validation ---'
rg -n -A25 -B15 \
'projectID|ProjectID|hash.*path|path.*hash|filepath\.Join.*project|capture-state|atomicWrite' \
internal/memory/store.go internal/mcpserver/mcpserver.go internal/mcpinit
printf '%s\n' '--- exact design references to trust, authorization, and marker fields ---'
rg -n -A8 -B8 \
'authorization|authoriz|trusted|active session|session context|capture-pending|capture-state|project_id|transcript_path|cwd' \
docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.mdRepository: wcatz/ghost Length of output: 50368 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- project ID callers and formats ---'
rg -n -A12 -B12 \
'EnsureProject\(|Create\(ctx, projectID|ResolveProject\(ctx' \
internal --glob '*.go' --glob '!**/*_test.go' | head -n 240
printf '%s\n' '--- design security text only ---'
rg -n -A10 -B10 \
'authorization|authoriz|trusted|active session|session context|capture-pending|capture-state|project_id|transcript_path|cwd' \
docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md
printf '%s\n' '--- filepath behavior for the planned state filename ---'
python3 - <<'PY'
import os
data_dir = "/data/ghost"
for project_id in ["project-a", "../other/state", "/tmp/other", "a/b", r"..\\other\\state"]:
print(project_id, "=>", os.path.join(data_dir, f"capture-state-{project_id}.json"))
PYRepository: wcatz/ghost Length of output: 24579 Bind The model controls both values. The marker stores no 🤖 Prompt for AI Agents |
||||
| 3. **Bound the slice.** Stream from the cursor up to `capture.max_transcript_bytes` | ||||
| (default 64 KiB), condensing tool results (large and noisy) to a short elision so the | ||||
| slice carries *conversation and tool intent*, not megabytes of file dumps. | ||||
|
Comment on lines
+71
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file="docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md"
printf '%s\n' '--- file status ---'
git ls-files --error-unmatch "$file"
printf '%s\n' '--- target section ---'
cat -n "$file" | sed -n '45,95p'
printf '%s\n' '--- cursor, offset, record, elision, and retry references ---'
rg -n -i -C 3 \
'cursor|offset|byte|partial|record|elid|transcript|max_transcript|capture|marker|resume|retry' \
"$file"Repository: wcatz/ghost Length of output: 15977 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository files related to transcript capture and hooks ---'
git ls-files | rg -i '(^|/)(capture|transcript|stophook|hook|memory).*|autonomous-memory|session-end'
printf '%s\n' '--- all cursor and transcript-offset implementations/references ---'
rg -n -i -C 4 \
'capture-state|capture-pending|max_transcript_bytes|transcript_path|transcript.*offset|byte offset|Seek|ReadAt|tool.result|tool_result|elision' \
--glob '!docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md' .
printf '%s\n' '--- design sections outside the target flow ---'
cat -n docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md | sed -n '1,44p;90,167p;168,245p'Repository: wcatz/ghost Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- existing transcript record shape and scanner behavior ---'
cat -n internal/mcpinit/stophook.go | sed -n '1,135p'
printf '%s\n' '--- prior transcript design guidance ---'
cat -n docs/superpowers/specs/2026-07-19-mcp-phase2-design.md | sed -n '75,110p'
cat -n docs/superpowers/plans/2026-07-19-mcp-phase2-design.md | sed -n '960,1048p'
printf '%s\n' '--- standalone boundary probe ---'
python3 - <<'PY'
import json
records = [
{"type": "assistant", "content": [{"type": "text", "text": "retain this"}]},
{"type": "user", "content": [{"type": "text", "text": "a record longer than the configured slice"}]},
]
raw = b"".join(json.dumps(r, separators=(",", ":")).encode() + b"\n" for r in records)
limit = len(json.dumps(records[0], separators=(",", ":")).encode()) + 7
first = raw[:limit]
print("first read ends inside second record:", first.endswith(b"\n") is False)
print("raw length:", len(raw), "limit:", limit)
print("first read decodes as complete JSONL records:")
for line in first.splitlines():
try:
json.loads(line)
print(" complete")
except json.JSONDecodeError:
print(" partial/unparseable")
print("correct retry offset is the start of the partial record:", raw.rfind(b"\n", 0, limit) + 1)
print("naive byte-limit offset would be:", limit)
PYRepository: wcatz/ghost Length of output: 8346 Define the capture cursor in original transcript bytes. The transcript is JSONL, but tool-result elision changes the sampled output. Specify that the cursor points to the original file, advances only after complete records, and retries a partial final record. Otherwise a byte-limit boundary can skip or repeat transcript content. 🤖 Prompt for AI Agents |
||||
| 4. **Sampling call.** `systemPrompt` = extraction instructions with the JSON schema | ||||
| `{"memories":[{"content","category","importance","confidence"}]}` and the category | ||||
| enum; `userContent` = the existing project memories (dedup hint) + the condensed | ||||
| slice. `MaxTokens` ~2000. | ||||
|
Comment on lines
+74
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift Bound the existing-memory dedup context.
🤖 Prompt for AI Agents |
||||
| 5. **Parse + validate.** Clamp importance/confidence to `[0,1]`, validate category. | ||||
| 6. **Confidence gate.** A candidate is **auto-saved** when | ||||
| `confidence ≥ capture.confidence_threshold` **and** | ||||
| `importance ≥ capture.importance_threshold`. Everything else is returned as a | ||||
|
Comment on lines
+78
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Validate capture configuration bounds. The design clamps candidate values but does not constrain configured thresholds. A negative threshold auto-saves every candidate, while a threshold above Also applies to: 131-136 🤖 Prompt for AI Agents |
||||
| *proposal* for the model to save explicitly. Auto-save reuses the same `Upsert` | ||||
| path `ghost_memory_save` uses, so FTS dedup, embedding, linking, and the | ||||
| un-resolve-on-write revive all fire; then `notifyProjectResource(projectID, "context")` | ||||
| like the other write tools. | ||||
|
Comment on lines
+74
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target specification ---'
sed -n '1,230p' docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md
printf '%s\n' '--- memory contracts and write path ---'
rg -n -A12 -B8 'type Memory struct|func .*Upsert|ghost_memory_save|source|tags' --glob '*.go' .
printf '%s\n' '--- autonomous capture references ---'
rg -n -A12 -B8 'autonomous|capture|transcript_path|project_id|SessionEnd|proposal' --glob '*' docs internal 2>/dev/null | head -n 500Repository: wcatz/ghost Length of output: 50368 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- specification excerpt ---'
nl -ba docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md | sed -n '60,95p;180,205p'
printf '%s\n' '--- candidate memory definitions ---'
rg -n -A20 -B5 'type Memory struct|type Memory ' --glob '*.go' .
printf '%s\n' '--- write-path symbols ---'
rg -n -A25 -B12 'ghost_memory_save|func .*Upsert|\.Upsert\(|Source|Tags' --glob '*.go' .Repository: wcatz/ghost Length of output: 50368 🏁 Script executed: printf '%s\n' '--- target lines ---'
nl -ba docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md | sed -n '60,95p;180,205p'
printf '%s\n' '--- relevant Go symbols ---'
rg -n -A20 -B8 'type Memory struct|ghost_memory_save|func .*Upsert|Source|Tags' --glob '*.go' .Repository: wcatz/ghost Length of output: 50368 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target specification sections ---'
nl -ba docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md | sed -n '68,90p;150,205p'
printf '%s\n' '--- Memory type ---'
rg -n -A22 -B4 '^type Memory struct' internal/memory
printf '%s\n' '--- Upsert definition and insert/update mapping ---'
rg -n -A110 -B12 '^func \(s \*Store\) Upsert' internal/memory/store.goRepository: wcatz/ghost Length of output: 7117 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- exact design text ---'
sed -n '68,90p;150,205p' docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md
printf '%s\n' '--- Memory struct ---'
python3 - <<'PY'
from pathlib import Path
for p in Path("internal/memory").glob("*.go"):
lines = p.read_text().splitlines()
for i, line in enumerate(lines):
if line.startswith("type Memory struct"):
print(f"{p}:{i+1}")
print("\n".join(f"{j+1}: {lines[j]}" for j in range(i, min(i+25, len(lines)))))
PY
printf '%s\n' '--- Upsert signature and first 100 lines ---'
python3 - <<'PY'
from pathlib import Path
p=Path("internal/memory/store.go")
lines=p.read_text().splitlines()
for i,line in enumerate(lines):
if line.startswith("func (s *Store) Upsert"):
print("\n".join(f"{j+1}: {lines[j]}" for j in range(i, min(i+115, len(lines)))))
break
PYRepository: wcatz/ghost Length of output: 10717 Specify the
🤖 Prompt for AI Agents
Comment on lines
+79
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift Define privacy controls before enabling automatic capture by default. With If transcripts can contain credentials or personal data, set the default to false or define these controls before release. Also applies to: 131-136 🤖 Prompt for AI Agents |
||||
| 7. **Empty-set guard.** Zero candidates → no write, just "nothing new to save". | ||||
| 8. **Commit.** Advance the capture cursor, clear the pending marker, and return a | ||||
| summary of saved vs proposed memories. | ||||
|
Comment on lines
+78
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file='docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md'
printf '%s\n' '--- cited design sections ---'
cat -n "$file" | sed -n '60,100p;165,195p'
printf '%s\n' '--- relevant tracked files ---'
git ls-files | rg -i '(memory|capture|session|store|transcript|hook)' | head -200
printf '%s\n' '--- references ---'
rg -n -i 'Store\.Create|Create\(|capture cursor|pending marker|pending_marker|transcript_path|confidence_threshold|SessionEnd|ghost_memory_save|notifyProjectResource|transaction|atomic' . --glob '!docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md' | head -300Repository: wcatz/ghost Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
spec='docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md'
store='internal/memory/store.go'
provider='internal/provider/provider.go'
printf '%s\n' '--- specification sections ---'
cat -n "$spec" | sed -n '1,60p;90,165p;191,280p;280,380p'
printf '%s\n' '--- store outline ---'
ast-grep outline "$store" --lang go
printf '%s\n' '--- provider interface ---'
cat -n "$provider" | sed -n '1,90p'
printf '%s\n' '--- store transaction and write methods ---'
rg -n -C 8 'func \(.*\) (Create|Upsert|Update|CreateLink)|Begin(Transaction|Tx)?|Commit|Rollback|WithTx|tx\.' "$store"
printf '%s\n' '--- capture state references ---'
rg -n -i -C 3 'capture-(state|pending)|pending marker|cursor|marker|advance.*cursor|clear.*marker' "$spec" internal docs --glob '*.go' --glob '*.md' | head -240Repository: wcatz/ghost Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
spec='docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md'
store='internal/memory/store.go'
provider='internal/provider/provider.go'
printf '%s\n' '--- specification sections ---'
cat -n "$spec" | sed -n '1,60p;90,165p;191,280p;280,380p'
printf '%s\n' '--- store outline ---'
ast-grep outline "$store" --lang go
printf '%s\n' '--- provider interface ---'
cat -n "$provider" | sed -n '1,90p'
printf '%s\n' '--- store transaction and write methods ---'
rg -n -C 8 'func \(.*\) (Create|Upsert|Update|CreateLink)|Begin(Transaction|Tx)?|Commit|Rollback|WithTx|tx\.' "$store"
printf '%s\n' '--- capture state references ---'
rg -n -i -C 3 'capture-(state|pending)|pending marker|cursor|advance.*cursor|clear.*marker' "$spec" internal docs --glob '*.go' --glob '*.md' | head -240Repository: wcatz/ghost Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
store='internal/memory/store.go'
printf '%s\n' '--- Create and Upsert implementations ---'
cat -n "$store" | sed -n '548,718p'
printf '%s\n' '--- capture commit/test wording ---'
python3 - <<'PY'
from pathlib import Path
p = Path('docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md')
text = p.read_text()
for term in ('sampling error', 'parse error', 'partial', 'crash', 'restart', 'retry', 'transaction', 'atomic'):
print(f'{term!r}:', [i + 1 for i, line in enumerate(text.splitlines()) if term.lower() in line.lower()])
PY
printf '%s\n' '--- focused verifier ---'
python3 - <<'PY'
from pathlib import Path
spec = Path('docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md').read_text()
store = Path('internal/memory/store.go').read_text()
provider = Path('internal/provider/provider.go').read_text()
flow = spec[spec.index('### 3.1'):spec.index('### 3.2')]
assert flow.index('auto-saved') < flow.index('Advance the capture cursor')
assert flow.index('Advance the capture cursor') < flow.index('clear the pending marker')
tests = spec[spec.index('## 8. Testing'):]
assert 'cursor advanced' in tests and 'marker cleared' in tests
missing_failure_cases = all(term not in spec.lower() for term in
('sampling error', 'parse error', 'partial upsert', 'process failure',
'restart recovery', 'atomic cursor', 'atomic marker'))
assert missing_failure_cases
upsert_start = store.index('func (s *Store) Upsert(')
upsert_end = store.index('\n}\n\n// ', upsert_start) + 2
upsert = store[upsert_start:upsert_end]
create_start = store.index('func (s *Store) Create(')
create_end = store.index('\n}\n\n// ', create_start) + 2
create = store[create_start:create_end]
print('documented write-before-commit order: PASS')
print('success-only cursor/marker test wording: PASS')
print('failure/restart/atomicity semantics absent: PASS')
print('Upsert method has transaction boundary:', 'BeginTx(' in upsert)
print('Create method has transaction boundary:', 'BeginTx(' in create)
print('MemoryStore exposes batch capture transaction:', 'Capture' in provider or 'BeginTx' in provider)
PYRepository: wcatz/ghost Length of output: 8441 Make the capture commit crash-safe.
Advance the cursor and clear the marker only after every write succeeds. Make memory writes and cursor/marker updates recoverable as one transaction or journaled, idempotent operation. Add tests for partial writes, restart recovery, and retries. 🤖 Prompt for AI Agents |
||||
|
|
||||
| ### 3.2 Refined `Stop` hook | ||||
|
|
||||
| When `capture.enabled`, `ghost hook stop` becomes: | ||||
|
|
||||
| 1. `stop_hook_active` → return early (unchanged — the turn is already continuing due to a | ||||
| prior hook, so never re-nudge). | ||||
| 2. Scan the transcript as today: count tool calls, `ghost_memory_save`/`ghost_save_global` | ||||
| calls, **and** `ghost_capture` calls. | ||||
| 3. No tool calls → return. | ||||
| 4. Saves or captures present → clear the pending marker and return (already recorded). | ||||
| 5. Read the pending marker. If it is for the **same session** and already marked | ||||
|
Comment on lines
+96
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file="docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md"
printf '%s\n' '--- target specification ---'
sed -n '1,180p' "$file"
printf '%s\n' '--- related symbols and tests ---'
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' \
'ghost_capture|ghost_memory_save|ghost_save_global|pending marker|pending_marker|SessionEnd|transcript_path|project_id' .Repository: wcatz/ghost Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- specification lines 88-125 ---'
sed -n '88,125p' docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md
printf '%s\n' '--- specification lines 125-180 ---'
sed -n '125,180p' docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.md
printf '%s\n' '--- current stop hook implementation ---'
sed -n '1,180p' internal/mcpinit/stophook.go
printf '%s\n' '--- current stop hook tests ---'
sed -n '1,155p' internal/mcpinit/stophook_test.go
printf '%s\n' '--- capture references limited to the target specification ---'
rg -n -C 4 'capture|pending|cursor|commit|failure|success' \
docs/superpowers/specs/2026-08-17-autonomous-memory-capture-design.mdRepository: wcatz/ghost Length of output: 27622 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import json
save_tools = {
"mcp__ghost__ghost_memory_save",
"mcp__ghost__ghost_save_global",
}
def scan(lines):
tool_calls = 0
saves = 0
captures = 0
for line in lines:
item = json.loads(line)
if item.get("type") != "assistant":
continue
for content in item.get("message", {}).get("content", []):
if content.get("type") != "tool_use":
continue
tool_calls += 1
name = content.get("name")
saves += name in save_tools
captures += name == "mcp__ghost__ghost_capture"
return tool_calls, saves, captures
def proposed_stop(marker, lines):
tool_calls, saves, captures = scan(lines)
if tool_calls == 0:
return marker, "return"
if saves or captures:
return None, "clear marker"
if marker and marker.get("nudged"):
return marker, "block"
return {"nudged": True}, "nudge"
for name, tool in [
("failed capture", "mcp__ghost__ghost_capture"),
("failed project save", "mcp__ghost__ghost_memory_save"),
("failed global save", "mcp__ghost__ghost_save_global"),
]:
marker, action = proposed_stop(
{"session_id": "s1", "transcript_path": "/tmp/t", "nudged": True},
[json.dumps({
"type": "assistant",
"message": {"content": [{"type": "tool_use", "name": tool}]},
})],
)
print(f"{name}: action={action!r}, marker={marker!r}")
PYRepository: wcatz/ghost Length of output: 311 Clear The Stop hook treats any 🤖 Prompt for AI Agents |
||||
| `nudged`, emit the original `decision:block` fallback and return. | ||||
| 6. Otherwise write the marker (`session_id`, `transcript_path`, `cwd`, `nudged: true`) and | ||||
| emit **`additionalContext`** (not `decision:block`): | ||||
| > "This session used tools but saved nothing to Ghost. Call `ghost_capture` to | ||||
| > extract discoveries from this session automatically." | ||||
|
|
||||
| The `nudged` flag is what makes "nudge once, then block" work. The immediate follow-up | ||||
| `Stop` after an `additionalContext` continuation carries `stop_hook_active: true` and | ||||
| returns at step 1 — so the block only fires on a *later* turn in the same session that | ||||
| still recorded nothing. Any save or capture clears the marker via step 4, resetting the | ||||
| cycle for the next burst of unrecorded work. The `stop_hook_active` guard plus Claude | ||||
| Code's 8-consecutive-continuation cap bound both mechanisms, exactly as today. | ||||
|
|
||||
| ### 3.3 `ghost hook session-end` (new event) | ||||
|
|
||||
| New `SessionEnd` hook moves the lifecycle spawns off the per-turn `Stop` path: | ||||
|
|
||||
| - `spawnResolveIfConfigured`, `spawnSupersedeIfConfigured` (and, from #297, | ||||
| `spawnReflectIfConfigured`) move from `HandleStopHook` into `HandleSessionEndHook`. | ||||
| - `SessionEnd` fires once at termination and supports only side effects (no decision | ||||
| control) — which is precisely what fire-and-forget detached spawns need. Its 1.5s | ||||
| budget comfortably covers the existing read-only project lookup + `exec.Command` | ||||
| start; raise it via the hook's `timeout` field if a slow first-run proves otherwise. | ||||
| - The `Stop` hook keeps the nudge (which genuinely needs per-turn + block/additionalContext); | ||||
| the lifecycle keeps its PID-file serialization and detached `--apply` semantics, | ||||
| unchanged. | ||||
|
|
||||
| ### 3.4 Config | ||||
|
|
||||
| ```go | ||||
| type CaptureConfig struct { | ||||
| Enabled bool `koanf:"enabled"` // default TRUE (opt-out) | ||||
| ConfidenceThreshold float64 `koanf:"confidence_threshold"` // default 0.8 | ||||
| ImportanceThreshold float64 `koanf:"importance_threshold"` // default 0.5 | ||||
| MaxTranscriptBytes int `koanf:"max_transcript_bytes"` // default 65536 | ||||
| } | ||||
| ``` | ||||
|
|
||||
| `reflection.auto_reflect` (default `false`) lands here per #297, completing the | ||||
| reflect/resolve/supersede trifecta on `SessionEnd`. | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 4. Guardrails (hard) | ||||
|
|
||||
| - **Hooks do zero DB/LLM work.** The `Stop` hook writes a marker and emits text only. | ||||
| `SessionEnd` performs the same small read-only project lookup + detached spawn the | ||||
| existing spawn helpers already do — no LLM, no write inline. | ||||
| - **Sampling only, no credits.** `ghost_capture` is constructible only with a live MCP | ||||
| session (`req.Session != nil`), mirroring `ghost_resolve`; headless invocation fails | ||||
| fast with a clear message. No Anthropic API key is ever touched by capture. | ||||
| - **No blind writes.** Confidence gate + empty-set guard + existing upsert dedup. A | ||||
| low-confidence or empty extraction never mutates the store. | ||||
| - **Never trap a session.** The nudge is `additionalContext`; the block is a bounded | ||||
| fallback; both are loop-protected by `stop_hook_active` and the 8-continuation cap. | ||||
| - **Untrusted content stays delimited.** Extracted memory text is stored data, and the | ||||
| existing `«…»` delimiter guard at injection continues to apply. The extraction prompt | ||||
| instructs the model to treat the existing-memories block as data, not instructions. | ||||
|
Comment on lines
+156
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift Treat the transcript slice as untrusted data. The guardrail explicitly treats the existing-memory block as data, but it does not apply the same rule to the transcript slice. Transcript content can contain instructions that cause memory poisoning or schema manipulation. Delimit both inputs and instruct the sampler to ignore instructions in both. Apply sensitive-content filtering before 🤖 Prompt for AI Agents |
||||
| - **Single-active-session marker.** `capture-pending.json` is one well-known file; two | ||||
| concurrent foreground sessions in different projects race on it (last-writer-wins). | ||||
| The explicit `transcript_path` arg is the escape hatch. Acceptable for a single-user | ||||
| local tool — documented here rather than silently ignored. | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 5. Data flow | ||||
|
|
||||
| ``` | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add a language to the data-flow fence. Markdownlint reports this fenced block without a language. Use Proposed documentation fix-```
+```text📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 168-168: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||
| Stop hook fires (per turn) | ||||
| → scan transcript (toolCalls, saves, captures) | ||||
| → toolCalls>0 && saves==0 && captures==0 ? | ||||
| → marker already marked `nudged` for this session? → decision:block (fallback) | ||||
| → else → write marker {session_id, transcript_path, cwd, nudged:true} | ||||
| → additionalContext: "call ghost_capture" | ||||
|
|
||||
| Model calls ghost_capture(project_id) | ||||
| → read marker → transcript_path | ||||
| → read capture cursor (byte offset) | ||||
| → stream bounded slice from offset | ||||
| → sampling(extraction prompt + existing memories + slice) | ||||
| → parse candidates | ||||
| → gate: auto-save high-conf/high-importance; propose rest | ||||
| → advance cursor, clear marker, report | ||||
|
|
||||
| SessionEnd hook fires (once) | ||||
| → spawn detached reflect --apply / resolve --apply / supersede --apply (PID-serialized) | ||||
| ``` | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 6. Components & boundaries | ||||
|
|
||||
| | Unit | Purpose | Depends on | | ||||
| |---|---|---| | ||||
| | `internal/capture` | transcript slicing, prompt build, JSON parse, confidence gate | `internal/ai` (sampling), `internal/memory` (types) | | ||||
| | `ai.SamplingProvider.Extract` | configurable-`MaxTokens` sampling call (vs 16-token `Classify`) | MCP `CreateMessage` | | ||||
| | `ghost_capture` tool handler | wires store + sampling + capture pkg; marker/cursor read-write | `internal/capture`, `internal/mcpserver` | | ||||
| | `HandleStopHook` refine | marker write + additionalContext nudge + block fallback | `internal/mcpinit/stophook.go` | | ||||
| | `HandleSessionEndHook` (new) | detached reflect/resolve/supersede spawns | `internal/mcpinit` | | ||||
| | `cmd/ghost` hook wiring | `ghost hook session-end` subcommand + init/status entries + `mcp__ghost__ghost_capture` permission allowlist | `internal/mcpinit` | | ||||
| | `internal/config` | `capture.*` + `reflection.auto_reflect` | koanf defaults | | ||||
|
|
||||
| Each unit is independently testable: the capture package against a fixture transcript; | ||||
| the tool handler against a fake sampler + seeded store (mirroring the existing | ||||
| `ghost_resolve` test); the stop/session-end hooks against canned stdin JSON. | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 7. Rejected alternatives | ||||
|
|
||||
| - **`PreCompact` trigger** — the natural "save before you forget" point, but the event | ||||
| supports only `decision: "block"` (it discards `systemMessage`/`continue` and has no | ||||
| `additionalContext`), so it cannot nudge the model to call a capture tool. Worse, | ||||
| blocking auto-compaction triggered by an already-returned context-limit error makes the | ||||
| current request fail. Deferred; a future "block manual `/compact` to remind the user" | ||||
| remains possible but is not autonomous. | ||||
| - **Detached Anthropic-API capture** (paid, works without a live session) — rejected in | ||||
| favor of free in-session sampling; a headless path could be added later behind | ||||
| `capture.provider` without disturbing this design. | ||||
| - **Model-driven notes** (`ghost_capture(notes=…)` where the model summarizes) — higher | ||||
| signal-to-noise but still depends on the model's diligence, defeating the autonomy | ||||
| goal. Transcript-driven was chosen. | ||||
| - **Daemon/cron for lifecycle** — out of scope; noted as future in #297. | ||||
| - **Keeping lifecycle on `Stop`** — per-turn prologue overhead is the bug this design | ||||
| fixes; `SessionEnd` is the semantically correct event. | ||||
|
|
||||
| --- | ||||
|
|
||||
| ## 8. Testing | ||||
|
|
||||
| - **Capture package** — fixture transcript: bounding at `max_transcript_bytes`, cursor | ||||
| resume across calls, tool-result elision, prompt construction, JSON parse, category/ | ||||
| range validation, confidence gate (auto-save vs proposal split), empty-set guard. | ||||
| - **Tool handler** — fake sampler + seeded store: saved list correct, proposals returned, | ||||
| cursor advanced, marker cleared; `req.Session == nil` fails fast. | ||||
| - **Stop hook** — canned stdin: marker written; nudge emitted on `toolCalls>0, saves==0, | ||||
| captures==0`; pass-through on saves/captures; block fallback on the second fire; silent | ||||
| on `stop_hook_active`. | ||||
|
Comment on lines
+236
to
+238
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Test the delayed block sequence. Lines [107-110] state that the immediate follow-up with Test: first fire → 🤖 Prompt for AI Agents |
||||
| - **Session-end hook** — detached spawns gated by PID file; no spawn when disabled or no | ||||
| matching project. | ||||
| - **Config** — `capture.*` defaults (`enabled=true`, thresholds 0.8/0.5); `auto_reflect` | ||||
| default `false`. | ||||
| - `go vet ./...` before commit; feature branch + PR. | ||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: wcatz/ghost
Length of output: 50370
🌐 Web query:
OpenCode GoUsageLimitError Retry-After reset interval retry.ts💡 Result:
In the OpenCode codebase, the GoUsageLimitError is handled within the retry.ts logic [1][2]. When this error is encountered, the code parses the Retry-After header from the API response to determine the reset interval [1][3]. The implementation extracts the retry-after value from the response headers, converts it from seconds into a human-readable format (days, hours, minutes), and uses this information to build a user-facing error message [1][4]. This message directs the user to their workspace settings to address the usage limit [1][3]. Community discussions have highlighted that because GoUsageLimitError represents a hard quota limit (e.g., weekly or monthly), the associated Retry-After values can be very large (e.g., hours or even days) [5]. Because the session retry logic may honor these long intervals, it can cause the client to wait for extended periods [5]. Developers have recommended treating GoUsageLimitError as a non-retryable or "fail-fast" condition to avoid these runaway session freezes, rather than allowing the system to attempt retries based on the long server-provided reset interval [5].
Citations:
🏁 Script executed:
Repository: wcatz/ghost
Length of output: 50370
🏁 Script executed:
Repository: wcatz/ghost
Length of output: 3206
Honor
Retry-Afterbefore exponential backoff.Read
Retry-Afterfrom theHTTPErrorheaders first. Parse its seconds value or HTTP-date, cap the resulting wait, and use the reset-message parser only as a fallback. SupportGoUsageLimitErrormessages with day, hour, and minute intervals.🤖 Prompt for AI Agents