claude-code-cache-fix 2.0.3 → 2.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-cache-fix",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Fixes prompt cache regression in Claude Code that causes up to 20x cost increase on resumed sessions",
5
5
  "type": "module",
6
6
  "exports": "./preload.mjs",
package/preload.mjs CHANGED
@@ -590,7 +590,17 @@ function isBookkeepingReminder(text) {
590
590
  // correctly.
591
591
  // --------------------------------------------------------------------------
592
592
 
593
- const CACHE_CONTROL_CANONICAL_MARKER = { type: "ephemeral", ttl: "1h" };
593
+ // Detected per-request from existing markers. Default 1h; downgraded to 5m
594
+ // if any existing block already carries ttl="5m" (Q5h=100% tier).
595
+ // The API rejects 1h markers after 5m markers, so all injected markers
596
+ // must match the lowest existing tier.
597
+ let _detectedTtlTier = "1h";
598
+
599
+ function getCanonicalMarker() {
600
+ return { type: "ephemeral", ttl: _detectedTtlTier };
601
+ }
602
+
603
+ const CACHE_CONTROL_CANONICAL_MARKER_LEGACY = { type: "ephemeral", ttl: "1h" };
594
604
 
595
605
  /**
596
606
  * Strip every cache_control marker from a single user message's content
@@ -777,7 +787,9 @@ const CACHE_CONTROL_STICKY_DIR = join(homedir(), ".claude", "cache-fix-state");
777
787
  // CC uses 1 on system[2] + cache_control_normalize places 1 on last user msg = 2 reserved.
778
788
  // Sticky can use at most 2 historical positions to stay within the 4-marker cap.
779
789
  const CACHE_CONTROL_STICKY_MAX_POSITIONS = 2;
780
- const CACHE_CONTROL_STICKY_DEFAULT_MARKER = { type: "ephemeral", ttl: "1h" };
790
+ function getCacheControlStickyDefaultMarker() {
791
+ return { type: "ephemeral", ttl: _detectedTtlTier };
792
+ }
781
793
 
782
794
  /**
783
795
  * Build the absolute state-file path for a given project key. Exported so
@@ -850,7 +862,7 @@ function readCacheControlStickyState(key) {
850
862
  marker:
851
863
  p.marker && typeof p.marker === "object" && typeof p.marker.type === "string"
852
864
  ? { ...p.marker }
853
- : { ...CACHE_CONTROL_STICKY_DEFAULT_MARKER },
865
+ : { ...getCacheControlStickyDefaultMarker() },
854
866
  });
855
867
  }
856
868
  return { version: 1, positions };
@@ -1800,6 +1812,24 @@ globalThis.fetch = async function (url, options) {
1800
1812
  debugLog("CACHE_FIX_DISABLED=1 — all bug fixes bypassed, monitoring active");
1801
1813
  }
1802
1814
 
1815
+ // Detect existing TTL tier from the payload. If any block already has
1816
+ // ttl="5m" (Q5h=100% tier), all injected markers must use 5m too —
1817
+ // the API rejects 1h after 5m in processing order (tools → system → messages).
1818
+ _detectedTtlTier = "1h";
1819
+ const allBlocks = [
1820
+ ...(Array.isArray(payload.system) ? payload.system : []),
1821
+ ...(Array.isArray(payload.messages) ? payload.messages.flatMap(m => Array.isArray(m.content) ? m.content : []) : []),
1822
+ ];
1823
+ for (const block of allBlocks) {
1824
+ if (block?.cache_control?.ttl === "5m") {
1825
+ _detectedTtlTier = "5m";
1826
+ break;
1827
+ }
1828
+ }
1829
+ if (_detectedTtlTier === "5m") {
1830
+ debugLog("TTL TIER DETECT: existing 5m markers found — all injected markers will use 5m");
1831
+ }
1832
+
1803
1833
  debugLog("--- API call to", urlStr);
1804
1834
  debugLog("message count:", payload.messages?.length);
1805
1835
 
@@ -2350,15 +2380,15 @@ globalThis.fetch = async function (url, options) {
2350
2380
  const existingCC = targetBlock?.cache_control;
2351
2381
  const canonicalAlreadyCorrect =
2352
2382
  existingCC &&
2353
- existingCC.type === CACHE_CONTROL_CANONICAL_MARKER.type &&
2354
- existingCC.ttl === CACHE_CONTROL_CANONICAL_MARKER.ttl;
2383
+ existingCC.type === getCanonicalMarker().type &&
2384
+ existingCC.ttl === getCanonicalMarker().ttl;
2355
2385
 
2356
2386
  if (!(canonicalAlreadyCorrect && countUserCacheControlMarkers(payload) === 1)) {
2357
2387
  // Strip all markers from user messages, then place canonical.
2358
2388
  for (const msg of payload.messages) stripCacheControlMarkers(msg);
2359
2389
  const tm = payload.messages[targetMsgIdx];
2360
2390
  const newContent = tm.content.slice();
2361
- newContent[targetBlockIdx] = { ...newContent[targetBlockIdx], cache_control: { ...CACHE_CONTROL_CANONICAL_MARKER } };
2391
+ newContent[targetBlockIdx] = { ...newContent[targetBlockIdx], cache_control: { ...getCanonicalMarker() } };
2362
2392
  payload.messages[targetMsgIdx] = { ...tm, content: newContent };
2363
2393
  ccMutated = true;
2364
2394
  }
@@ -2423,7 +2453,8 @@ globalThis.fetch = async function (url, options) {
2423
2453
  debugLog(`SKIPPED: TTL injection (${requestType} set to 'none' — pass-through)`);
2424
2454
  recordFixResult("ttl", "skipped");
2425
2455
  } else {
2426
- const ttlParam = ttlValue === "5m" ? "5m" : "1h";
2456
+ // Respect detected tier: if existing blocks have 5m, never inject 1h
2457
+ const ttlParam = ttlValue === "5m" || _detectedTtlTier === "5m" ? "5m" : "1h";
2427
2458
  let ttlInjected = 0;
2428
2459
  payload.system = payload.system.map((block) => {
2429
2460
  if (block.cache_control?.type === "ephemeral" && !block.cache_control.ttl) {
@@ -2835,7 +2866,8 @@ export {
2835
2866
  isBookkeepingReminder,
2836
2867
  stripCacheControlMarkers,
2837
2868
  countUserCacheControlMarkers,
2838
- CACHE_CONTROL_CANONICAL_MARKER,
2869
+ CACHE_CONTROL_CANONICAL_MARKER_LEGACY,
2870
+ getCanonicalMarker,
2839
2871
  normalizeToolUseInputsInBody,
2840
2872
  computeStickyMessageHash,
2841
2873
  cacheControlStickyStatePath,
@@ -2844,6 +2876,6 @@ export {
2844
2876
  readCacheControlStickyState,
2845
2877
  writeCacheControlStickyState,
2846
2878
  CACHE_CONTROL_STICKY_MAX_POSITIONS,
2847
- CACHE_CONTROL_STICKY_DEFAULT_MARKER,
2879
+ getCacheControlStickyDefaultMarker,
2848
2880
  _pinnedBlocks, // exported so tests can reset between runs
2849
2881
  };
@@ -0,0 +1,138 @@
1
+ # manual-compact.sh — Manual Compaction for 1M Context Hack Sessions
2
+
3
+ ## Purpose
4
+
5
+ When using the 1M context window hack (`DISABLE_COMPACT=1` + `CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000`), the `/compact` command is disabled by CC. This tool provides a manual compaction alternative: extract the conversation, summarize it via Claude, and restore context after `/clear`.
6
+
7
+ **This tool is specifically for sessions running the 1M hack.** If you have `/compact` available, use that instead — it's built-in, integrated, and handles the full compaction lifecycle automatically.
8
+
9
+ ## How It Works
10
+
11
+ 1. Extracts conversation turns from the session JSONL transcript
12
+ 2. Splits turns into three weighted segments:
13
+ - **Foundational** (first 20%) — truncated to 200 chars each
14
+ - **Working** (middle 40%) — truncated to 400 chars each
15
+ - **Active** (last 40%) — preserved up to 2000 chars each
16
+ 3. Sends the weighted extract to Claude Sonnet for summarization
17
+ 4. Produces a structured summary optimized for agent handoff
18
+
19
+ The weighting ensures recent active work (the part you're most likely to need) gets full detail, while earlier completed work is compressed.
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ # By project directory (recommended) — auto-finds the most recent session
25
+ manual-compact.sh ~/git_repos/myproject
26
+
27
+ # By project directory with user context
28
+ manual-compact.sh ~/git_repos/myproject /tmp/context.txt
29
+
30
+ # By direct JSONL path (if you know the exact session)
31
+ manual-compact.sh ~/.claude/projects/-home-user-git-repos-myproject/abc123.jsonl
32
+
33
+ # By direct JSONL path with user context
34
+ manual-compact.sh ~/.claude/projects/-home-user-git-repos-myproject/abc123.jsonl /tmp/context.txt
35
+ ```
36
+
37
+ When you pass a project directory, the tool:
38
+ 1. Converts it to CC's internal project path format
39
+ 2. Finds the most recently modified session JSONL
40
+ 3. Shows you the session details (modified date, size)
41
+ 4. **Asks for confirmation** before proceeding
42
+
43
+ ### WARNING: Wrong Session = Wrong Context
44
+
45
+ **If you select the wrong session JSONL, the summary will be from a completely different conversation.** Loading that summary after `/clear` will inject false context — the agent will confidently act on information from another session, another project, or another agent's work.
46
+
47
+ Always:
48
+ - Verify the session timestamp matches your active session
49
+ - Review the summary output before feeding it to an agent
50
+ - When in doubt, check the last few lines of the JSONL to confirm it's the right conversation
51
+
52
+ ### Example: Basic Compaction
53
+
54
+ ```bash
55
+ ./tools/manual-compact.sh ~/git_repos/kanfei-nowcast-e3b
56
+ ```
57
+
58
+ ```
59
+ Project directory: /home/manager/git_repos/kanfei_nowcast_e3b
60
+ Auto-detected session: db11f377-4ca8-4fc3-9b6d-1069da58c1b2.jsonl
61
+ Modified: 2026-04-19 13:26:42
62
+ Size: 4.8M
63
+
64
+ Is this the correct session? [Y/n]
65
+ ```
66
+
67
+ Output: `/tmp/db11f377-...-compact-summary.txt`
68
+
69
+ ### Example: With User Context
70
+
71
+ If there's specific context you know the summary might miss:
72
+
73
+ ```bash
74
+ echo "The MR2 OOM debugging took 3 days. The PR #75 architectural recommendation
75
+ was max(dualpol_lr, hail_lr) for correlation grouping." > /tmp/context.txt
76
+
77
+ ./tools/manual-compact.sh ~/git_repos/kanfei-nowcast-e3b /tmp/context.txt
78
+ ```
79
+
80
+ The user context is injected into the summarization prompt, ensuring those details appear in the output.
81
+
82
+ ### Restoring Context After /clear
83
+
84
+ In the CC session:
85
+
86
+ ```
87
+ /clear
88
+ ```
89
+
90
+ Then as your first message:
91
+
92
+ ```
93
+ Read /tmp/<session-id>-compact-summary.txt for context on where we left off.
94
+ ```
95
+
96
+ ## Limitations
97
+
98
+ ### This tool is a workaround, not a replacement for /compact
99
+
100
+ - `/compact` operates inside CC with full access to the internal message array, system prompt, tool schemas, and session state. This tool only sees the JSONL transcript, which is a subset.
101
+ - `/compact` preserves CC's internal state (tool registration, MCP connections, plugin state). This tool + `/clear` resets all of that. The agent must re-establish any stateful connections.
102
+ - `/compact` is atomic — one command, seamless continuation. This tool requires `/clear` + paste, which is a hard context boundary.
103
+
104
+ ### Summary fidelity
105
+
106
+ Tested at ~95% fidelity for active work resumption, ~70% for broader project context. Gaps typically include:
107
+
108
+ - **Operational debugging history** — multi-day debugging sagas compress away
109
+ - **Timeline information** — the summary doesn't indicate when things happened or how long they took
110
+ - **Depth of architectural discussions** — detailed technical recommendations get compressed to one-liners
111
+ - **Background process context** — overnight watchers, cron monitoring, polling patterns
112
+
113
+ Use the user context file to fill known gaps.
114
+
115
+ ### Token cost
116
+
117
+ The summarization call costs tokens against your Q5h quota. At ~50K extract tokens through Sonnet, expect ~1-2% Q5h per compaction. This is comparable to what `/compact` costs.
118
+
119
+ ### Requires Claude Sonnet access
120
+
121
+ The tool uses `claude --print --model claude-sonnet-4-6` for summarization. Sonnet is used instead of Opus to minimize Q5h impact. If Sonnet is unavailable, change the model in the script.
122
+
123
+ ## Why the 1M Hack Disables /compact
124
+
125
+ The 1M context hack works by setting `DISABLE_COMPACT=1`, which CC reads as "disable all compaction." CC's code uses a single env var to control both:
126
+ - The context window calculation (`ff()` returns 1M when `DISABLE_COMPACT=1`)
127
+ - The `/compact` command availability (`isEnabled: () => !DISABLE_COMPACT`)
128
+
129
+ These are coupled in CC's source — there is no way to get 1M context AND `/compact` simultaneously without CC code changes. The coupling is in the CC binary, not in our interceptor.
130
+
131
+ We attempted to toggle `DISABLE_COMPACT` via the interceptor (set during API calls, unset between turns), but CC registers available commands at startup before any API call, so the toggle cannot re-enable `/compact` after session start.
132
+
133
+ ## Requirements
134
+
135
+ - Claude Code v2.1.112 (the last Node.js version — v2.1.113+ uses Bun)
136
+ - The cache-fix interceptor loaded via `NODE_OPTIONS=--import`
137
+ - `DISABLE_COMPACT=1` and `CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000` set
138
+ - `claude` CLI available in PATH (used for summarization)
@@ -0,0 +1,212 @@
1
+ #!/bin/bash
2
+ # manual-compact.sh — Generate a compaction summary for a CC session
3
+ #
4
+ # Usage:
5
+ # manual-compact.sh <project-dir-or-session-jsonl> [user-context-file]
6
+ #
7
+ # Accepts either:
8
+ # - A project working directory (e.g. ~/git_repos/myproject)
9
+ # → auto-finds the most recent session JSONL
10
+ # - A direct path to a session JSONL file
11
+ #
12
+ # Produces a summary at /tmp/<session-id>-compact-summary.txt
13
+ # that can be pasted or referenced after /clear.
14
+ #
15
+ # The optional user-context-file is additional context the user wants
16
+ # preserved in the summary (equivalent to /compact <instructions>).
17
+ #
18
+ # WARNING: Using the wrong session JSONL will produce a summary from
19
+ # a DIFFERENT conversation. Loading that into your session after /clear
20
+ # will inject completely wrong context. Always verify the output before
21
+ # feeding it to an agent.
22
+
23
+ set -euo pipefail
24
+
25
+ if [ $# -lt 1 ]; then
26
+ echo "Usage: $0 <project-dir-or-session-jsonl> [user-context-file]"
27
+ echo ""
28
+ echo "Generates a compaction summary from a CC session JSONL transcript."
29
+ echo "After /clear, reference the output file to restore context."
30
+ echo ""
31
+ echo "Arguments:"
32
+ echo " <project-dir> Working directory of the CC session (e.g. ~/git_repos/myproject)"
33
+ echo " Auto-detects the most recent session JSONL."
34
+ echo " <session-jsonl> Direct path to a session JSONL file."
35
+ echo " [user-context] Optional file with additional context to preserve."
36
+ echo ""
37
+ echo "WARNING: Verify the output summary before loading it after /clear."
38
+ echo " A wrong session JSONL = wrong context = confused agent."
39
+ exit 1
40
+ fi
41
+
42
+ INPUT="$1"
43
+ USER_CONTEXT_FILE="${2:-}"
44
+
45
+ # Determine if input is a directory or a JSONL file
46
+ if [ -d "$INPUT" ]; then
47
+ # Convert project directory to CC's project path format
48
+ REAL_PATH=$(realpath "$INPUT")
49
+ # CC replaces / with - and prepends -
50
+ PROJECT_KEY=$(echo "$REAL_PATH" | sed 's|/|-|g')
51
+ PROJECT_DIR="$HOME/.claude/projects/${PROJECT_KEY}"
52
+
53
+ if [ ! -d "$PROJECT_DIR" ]; then
54
+ echo "ERROR: No CC project found for directory: $INPUT"
55
+ echo " Expected: $PROJECT_DIR"
56
+ echo ""
57
+ echo "Available projects:"
58
+ ls -d ~/.claude/projects/*/ 2>/dev/null | head -10
59
+ exit 1
60
+ fi
61
+
62
+ # Find the most recent JSONL (exclude subdirectories like subagents/)
63
+ JSONL=$(find "$PROJECT_DIR" -maxdepth 1 -name "*.jsonl" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
64
+
65
+ if [ -z "$JSONL" ]; then
66
+ echo "ERROR: No session JSONL found in: $PROJECT_DIR"
67
+ exit 1
68
+ fi
69
+
70
+ echo "Project directory: $INPUT"
71
+ echo "Auto-detected session: $(basename "$JSONL")"
72
+ echo " Modified: $(stat -c '%y' "$JSONL" | cut -d'.' -f1)"
73
+ echo " Size: $(du -h "$JSONL" | cut -f1)"
74
+ echo ""
75
+ read -p "Is this the correct session? [Y/n] " CONFIRM
76
+ if [[ "${CONFIRM:-Y}" =~ ^[Nn] ]]; then
77
+ echo ""
78
+ echo "Available sessions in $PROJECT_DIR:"
79
+ ls -lt "$PROJECT_DIR"/*.jsonl 2>/dev/null | awk '{print " " $6, $7, $8, $NF}'
80
+ echo ""
81
+ echo "Re-run with the specific JSONL path."
82
+ exit 1
83
+ fi
84
+ elif [ -f "$INPUT" ]; then
85
+ JSONL="$INPUT"
86
+ else
87
+ echo "ERROR: $INPUT is not a directory or file."
88
+ exit 1
89
+ fi
90
+
91
+ SESSION_ID=$(basename "$JSONL" .jsonl)
92
+ OUTPUT="/tmp/${SESSION_ID}-compact-summary.txt"
93
+ EXTRACT="/tmp/${SESSION_ID}-conv-extract.txt"
94
+
95
+ echo ""
96
+ echo "Extracting conversation from: $JSONL"
97
+
98
+ # Extract conversation turns, keeping more detail for recent turns
99
+ python3 << PYEOF
100
+ import json, sys
101
+
102
+ conversation = []
103
+ with open("$JSONL") as f:
104
+ for line in f:
105
+ try:
106
+ d = json.loads(line.strip())
107
+ if d.get('type') == 'user':
108
+ msg = d.get('message', {})
109
+ content = msg.get('content', '')
110
+ if isinstance(content, str) and len(content.strip()) > 0:
111
+ if content.startswith('<local-command') or content.startswith('<command-name>'):
112
+ continue
113
+ conversation.append(('user', content))
114
+ elif isinstance(content, list):
115
+ texts = []
116
+ for b in content:
117
+ if isinstance(b, dict):
118
+ if b.get('type') == 'text' and b.get('text'):
119
+ t = b['text']
120
+ if not t.startswith('<local-command') and not t.startswith('<command-name>'):
121
+ texts.append(t)
122
+ elif b.get('type') == 'tool_result' and b.get('content'):
123
+ c = b['content']
124
+ if isinstance(c, str):
125
+ texts.append(c)
126
+ elif isinstance(c, list):
127
+ for tb in c:
128
+ if isinstance(tb, dict) and tb.get('text'):
129
+ texts.append(tb['text'])
130
+ if texts:
131
+ conversation.append(('user', ' '.join(texts)))
132
+ elif d.get('type') == 'assistant':
133
+ msg = d.get('message', {})
134
+ content = msg.get('content', [])
135
+ if isinstance(content, list):
136
+ texts = [b.get('text', '') for b in content if isinstance(b, dict) and b.get('type') == 'text' and b.get('text')]
137
+ if texts:
138
+ conversation.append(('assistant', ' '.join(texts)))
139
+ except:
140
+ pass
141
+
142
+ total = len(conversation)
143
+ if total == 0:
144
+ print("No conversation found.", file=sys.stderr)
145
+ sys.exit(1)
146
+
147
+ # Split into three segments with different detail levels:
148
+ # - First 20%: truncate to 200 chars each (foundational context)
149
+ # - Middle 40%: truncate to 400 chars each (working context)
150
+ # - Last 40%: full text up to 2000 chars each (active work — most important)
151
+ seg1_end = int(total * 0.2)
152
+ seg2_end = int(total * 0.6)
153
+
154
+ with open("$EXTRACT", 'w') as f:
155
+ f.write("=== FOUNDATIONAL CONTEXT (early session) ===\n\n")
156
+ for role, text in conversation[:seg1_end]:
157
+ f.write(f"[{role}]: {text[:200]}\n\n")
158
+
159
+ f.write("\n=== WORKING CONTEXT (mid session) ===\n\n")
160
+ for role, text in conversation[seg1_end:seg2_end]:
161
+ f.write(f"[{role}]: {text[:400]}\n\n")
162
+
163
+ f.write("\n=== ACTIVE WORK (recent — preserve in full detail) ===\n\n")
164
+ for role, text in conversation[seg2_end:]:
165
+ f.write(f"[{role}]: {text[:2000]}\n\n")
166
+
167
+ import os
168
+ size = os.path.getsize("$EXTRACT")
169
+ print(f"Extracted {total} turns ({size:,} bytes, ~{size//4:,} est. tokens)")
170
+ print(f" Foundational: {seg1_end} turns (truncated to 200 chars)")
171
+ print(f" Working: {seg2_end - seg1_end} turns (truncated to 400 chars)")
172
+ print(f" Active: {total - seg2_end} turns (up to 2000 chars)")
173
+ PYEOF
174
+
175
+ # Build the summarization prompt
176
+ USER_CONTEXT=""
177
+ if [ -n "$USER_CONTEXT_FILE" ] && [ -f "$USER_CONTEXT_FILE" ]; then
178
+ USER_CONTEXT=$(cat "$USER_CONTEXT_FILE")
179
+ echo "User context loaded from: $USER_CONTEXT_FILE"
180
+ fi
181
+
182
+ PROMPT="Summarize this conversation for context continuity after a /clear.
183
+
184
+ CRITICAL PRIORITIES (in order):
185
+ 1. ACTIVE WORK STATE — What is the agent doing RIGHT NOW? What branch, what uncommitted changes, what task is in progress, what was the last action taken? This is the most important section. Be precise about exactly where things stand — do not understate progress.
186
+ 2. RECENT DECISIONS — Key decisions made in the last ~20% of the conversation and their rationale.
187
+ 3. PENDING NEXT STEPS — What was about to happen next? What was queued?
188
+ 4. COMPLETED WORK — PRs merged, issues closed, features shipped. Brief — the git history has the details.
189
+ 5. FOUNDATIONAL CONTEXT — Agent identity, repo location, key collaborators, infrastructure. Brief.
190
+
191
+ FORMAT: Use headers and bullet points. Be specific about file paths, branch names, commit SHAs, function names. The agent reading this will have zero prior context — every detail that matters must be explicit.
192
+
193
+ DO NOT understate progress on in-flight work. If the last 20% of the conversation shows implementation was done, say it was done — do not say 'investigation started'."
194
+
195
+ if [ -n "$USER_CONTEXT" ]; then
196
+ PROMPT="$PROMPT
197
+
198
+ ADDITIONAL USER CONTEXT TO PRESERVE:
199
+ $USER_CONTEXT"
200
+ fi
201
+
202
+ echo ""
203
+ echo "Sending to Claude for summarization..."
204
+
205
+ cat "$EXTRACT" | claude --print --model claude-sonnet-4-6 "$PROMPT" > "$OUTPUT" 2>/dev/null
206
+
207
+ SIZE=$(wc -c < "$OUTPUT")
208
+ echo ""
209
+ echo "Summary generated: $OUTPUT ($SIZE bytes)"
210
+ echo ""
211
+ echo "To restore context after /clear:"
212
+ echo " Read $OUTPUT for context on where we left off."