-
Notifications
You must be signed in to change notification settings - Fork 1
feat(session-recap): configurable window via AMICODE_SESSION_RECAP_WINDOW_DAYS #548
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -727,6 +727,7 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean | |
| kind: "data-storage-defaults", | ||
| databasePath: shorten(defaultDbPath), | ||
| configDir: shorten(defaultConfigDir), | ||
| recapWindowDays: 7, | ||
| tab: msg.tab, | ||
| }); | ||
| return true; | ||
|
|
@@ -739,6 +740,9 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean | |
| const configDir = typeof (msg as { configDir?: unknown }).configDir === "string" | ||
| ? (msg as unknown as { configDir: string }).configDir.trim().replace(/^~/, os.homedir()) | ||
| : ""; | ||
| const recapWindowDays = typeof (msg as { recapWindowDays?: unknown }).recapWindowDays === "number" | ||
| ? (msg as unknown as { recapWindowDays: number }).recapWindowDays | ||
| : 7; | ||
|
Comment on lines
+743
to
+745
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 Reject non-finite recap windows before persisting.
Proposed fix- if (recapWindowDays >= 1) {
+ if (Number.isFinite(recapWindowDays) && recapWindowDays >= 1) {Also applies to: 811-816 🤖 Prompt for AI Agents |
||
|
|
||
| const reply: { | ||
| source: "amicode"; kind: "data-storage-status"; tab?: string; | ||
|
|
@@ -804,6 +808,12 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean | |
| "configDir", configDir, vscode.ConfigurationTarget.Global, | ||
| ); | ||
| } | ||
| // Recap window: always valid (clamped to >= 1 on the client side) | ||
| if (recapWindowDays >= 1) { | ||
| void vscode.workspace.getConfiguration("amicode").update( | ||
| "sessionRecapWindowDays", recapWindowDays, vscode.ConfigurationTarget.Global, | ||
| ); | ||
| } | ||
|
|
||
| // Restart the server so it picks up the new env vars | ||
| if (reply.databaseValid && reply.configValid) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -316,8 +316,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> { | |
| const cfg = vscode.workspace.getConfiguration("amicode"); | ||
| const sessionDb = cfg.get<string>("sessionDatabase", ""); | ||
| const configDirOverride = cfg.get<string>("configDir", ""); | ||
| const recapWindow = cfg.get<number>("sessionRecapWindowDays", 0); | ||
| if (sessionDb) env.OPENCODE_DB = sessionDb; | ||
| if (configDirOverride) env.OPENCODE_CONFIG_DIR = configDirOverride; | ||
| if (recapWindow > 0) env.AMICODE_SESSION_RECAP_WINDOW_DAYS = String(recapWindow); | ||
|
Comment on lines
+319
to
+322
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 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Expect: buildServerSpawnEnv either removes or does not inherit this key.
rg -n -C 10 \
'buildServerSpawnEnv|AMICODE_SESSION_RECAP_WINDOW_DAYS|process\.env' \
packages/extensionRepository: harmoniqs/amicode Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- definitions and call sites ---'
rg -n -C 8 'buildServerSpawnEnv|spawnEnv|AMICODE_SESSION_RECAP_WINDOW_DAYS' \
packages/extension/src/extension.ts packages/extension/src \
-g '*.ts' -g '!**/test/**' | head -n 240
printf '%s\n' '--- extension structure ---'
ast-grep outline packages/extension/src/extension.ts --match 'buildServerSpawnEnv' --view expanded 2>/dev/null || trueRepository: harmoniqs/amicode Length of output: 20467 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- buildServerSpawnEnv implementation ---'
sed -n '180,290p' packages/extension/src/server_auth.ts
printf '%s\n' '--- recap resolver and consumers ---'
rg -n -C 8 'resolveWindowDays|AMICODE_SESSION_RECAP_WINDOW_DAYS|sessionRecapWindowDays' \
packages/extension/src packages/extension/package.json packages/extension/test \
-g '*.ts' -g '*.json' | head -n 260
printf '%s\n' '--- fleet branch and server launch paths ---'
sed -n '520,680p' packages/extension/src/extension.ts
sed -n '1280,1370p' packages/extension/src/extension.tsRepository: harmoniqs/amicode Length of output: 28706 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- passthrough allowlist ---'
rg -n -C 12 'SANDBOX_ENV_PASSTHROUGH' packages/extension/src/server_auth.ts
printf '%s\n' '--- exact relevant tests ---'
rg -n -C 10 'buildServerSpawnEnv|SANDBOX_ENV_PASSTHROUGH|recap' \
packages/extension/test packages/extension/src/server_auth.ts \
-g '*.ts' | head -n 260
printf '%s\n' '--- deterministic source check ---'
python3 - <<'PY'
from pathlib import Path
auth = Path("packages/extension/src/server_auth.ts").read_text()
ext = Path("packages/extension/src/extension.ts").read_text()
allowlist_start = auth.find("SANDBOX_ENV_PASSTHROUGH")
allowlist_end = auth.find("]);", allowlist_start)
allowlist = auth[allowlist_start:allowlist_end + 3]
builder_start = auth.find("export function buildServerSpawnEnv")
builder_end = auth.find("\n}", builder_start)
builder = auth[builder_start:builder_end + 2]
print("recap_in_allowlist =", "AMICODE_SESSION_RECAP_WINDOW_DAYS" in allowlist)
print("builder_uses_spread_process_env =", "...process.env" in builder)
print("builder_reads_env_source_for_recap =", "envSource.AMICODE_SESSION_RECAP_WINDOW_DAYS" in builder)
print("extension_deletes_recap_on_nonpositive =", "delete env.AMICODE_SESSION_RECAP_WINDOW_DAYS" in ext)
print("extension_conditional_override =", "if (recapWindow > 0) env.AMICODE_SESSION_RECAP_WINDOW_DAYS" in ext)
PYRepository: harmoniqs/amicode Length of output: 26444 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ServerManager child environment construction ---'
rg -n -C 14 'spawn\(|env:|process\.env|this\.env' packages/extension/src/server_manager.ts
printf '%s\n' '--- server manager options and tests for environment precedence ---'
sed -n '1,220p' packages/extension/src/server_manager.ts
rg -n -C 10 'AMICODE_SESSION_RECAP_WINDOW_DAYS|spawn env|inherits|process\.env' \
packages/extension/test/server_manager.test.ts packages/extension/test/server_auth.test.tsRepository: harmoniqs/amicode Length of output: 30562 Clear the inherited recap-window override for non-positive settings.
🤖 Prompt for AI Agents🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Expect: a remote recap-window propagation path, or an explicit local-only contract.
rg -n -C 8 \
'fleetClient|spawnEnv|sessionRecapWindowDays|AMICODE_SESSION_RECAP_WINDOW_DAYS' \
packages/extensionRepository: harmoniqs/amicode Length of output: 30850 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- fleet-client path ---'
sed -n '530,665p' packages/extension/src/extension.ts
printf '%s\n' '--- spawn environment implementation and callers ---'
rg -n -C 10 'function buildServerSpawnEnv|const buildServerSpawnEnv|export .*buildServerSpawnEnv|buildServerSpawnEnv\(' packages/extension/src packages/extension/test
printf '%s\n' '--- recap setting and fleet documentation/tests ---'
rg -n -C 6 'sessionRecapWindowDays|AMICODE_SESSION_RECAP_WINDOW_DAYS|fleet client|fleet-client|Go Standalone|tunnel' \
packages/extension README.md docs 2>/dev/null || trueRepository: harmoniqs/amicode Length of output: 50374 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- server environment builder ---'
sed -n '226,285p' packages/extension/src/server_auth.ts
printf '%s\n' '--- fleet ADR mode and configuration requirements ---'
rg -n -C 8 \
'client|canonical|setting|configuration|config|attach|tunnel|local fallback|server mode|recap|session' \
docs/adr/0005-managed-fleet.md
printf '%s\n' '--- all focused recap references ---'
rg -n \
'sessionRecapWindowDays|AMICODE_SESSION_RECAP_WINDOW_DAYS|resolveWindowDays|session recap' \
packages/extension README.md docs --glob '!**/node_modules/**'Repository: harmoniqs/amicode Length of output: 11572 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- configuration declaration ---'
sed -n '180,270p' packages/extension/package.json
printf '%s\n' '--- fleet configuration and client/server mode symbols ---'
rg -n -C 5 \
'configurationDefaults|machine|sessionRecapWindowDays|serverMode|role|canonical|client' \
packages/extension/package.json packages/extension/src packages/extension/test \
--glob '!**/extension.ts' --glob '!**/server_auth.test.ts'
printf '%s\n' '--- recap plugin loading and configuration boundary ---'
rg -n -C 8 \
'session_recap|opencode-plugin|OPENCODE_CONFIG_CONTENT|buildOpencodeConfigContent' \
packages/extension/src packages/extension/opencode-plugin packages/extension/test \
--glob '!**/extension.ts'Repository: harmoniqs/amicode Length of output: 50374 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
from pathlib import Path
p = Path("packages/extension/package.json")
data = json.loads(p.read_text())
configs = data.get("contributes", {}).get("configuration", {})
print("configuration entries:", len(configs) if isinstance(configs, list) else type(configs).__name__)
def walk(value, path=""):
if isinstance(value, dict):
for k, v in value.items():
current = f"{path}.{k}" if path else k
if k == "amicode.sessionRecapWindowDays" or "sessionRecapWindowDays" in k:
print(current, json.dumps(v, indent=2))
walk(v, current)
elif isinstance(value, list):
for i, v in enumerate(value):
walk(v, f"{path}[{i}]")
walk(configs)
PY
printf '%s\n' '--- exact package declaration ---'
rg -n -C 12 \
'"amicode\.sessionRecapWindowDays"|scope' \
packages/extension/package.json
printf '%s\n' '--- exact fleet client/server setting references ---'
rg -n -C 4 \
'sessionRecapWindowDays|AMICODE_SESSION_RECAP_WINDOW_DAYS' \
packages/extension/src packages/extension/opencode-plugin packages/extension/test packages/extension/package.jsonRepository: harmoniqs/amicode Length of output: 10242 Define fleet-client behavior for 🤖 Prompt for AI Agents |
||
| return (currentSpawnEnv = env); | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,8 @@ import { | |
| readCachedRecap, | ||
| writeCachedRecap, | ||
| buildRecentSessionsBlock, | ||
| resolveWindowDays, | ||
| RECAP_WINDOW_DAYS, | ||
| NOISE_TITLE_PREFIXES, | ||
| MIN_ASSISTANT_MESSAGES, | ||
| MAX_RECAPS, | ||
|
|
@@ -167,7 +169,7 @@ describe("composeRecapText — recap string composition", () => { | |
| // ── composeMarkdown ────────────────────────────────────────────────────────── | ||
|
|
||
| describe("composeMarkdown — final prompt section composition", () => { | ||
| it("starts with the heading", () => { | ||
| it("starts with the heading (default window)", () => { | ||
|
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 Isolate the default-window test from
🤖 Prompt for AI Agents |
||
| const recaps: SessionRecap[] = [{ | ||
| session_id: "ses_1", | ||
| title: "Test", | ||
|
|
@@ -179,6 +181,18 @@ describe("composeMarkdown — final prompt section composition", () => { | |
| expect(md.startsWith("## Recent sessions (last 7 days)")).toBe(true); | ||
| }); | ||
|
|
||
| it("heading reflects custom windowDays parameter", () => { | ||
| const recaps: SessionRecap[] = [{ | ||
| session_id: "ses_1", | ||
| title: "Test", | ||
| created: "2026-08-23T10:30:00.000Z", | ||
| recap: "Did some stuff", | ||
| summarized_at: "2026-08-23T14:00:00.000Z", | ||
| }]; | ||
| const md = composeMarkdown(recaps, 14); | ||
| expect(md.startsWith("## Recent sessions (last 14 days)")).toBe(true); | ||
| }); | ||
|
|
||
| it("renders date and time for each entry", () => { | ||
| const recaps: SessionRecap[] = [{ | ||
| session_id: "ses_1", | ||
|
|
@@ -278,6 +292,62 @@ describe("cache — read/write SessionRecap to disk", () => { | |
| }); | ||
| }); | ||
|
|
||
| // ── resolveWindowDays — env-configurable window ────────────────────────────── | ||
|
|
||
| describe("resolveWindowDays — environment override of RECAP_WINDOW_DAYS", () => { | ||
| const origEnv = process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS; | ||
|
|
||
| afterEach(() => { | ||
| if (origEnv === undefined) delete process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS; | ||
| else process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = origEnv; | ||
| }); | ||
|
|
||
| it("returns default (7) when env is unset", () => { | ||
| delete process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS; | ||
| expect(resolveWindowDays()).toBe(RECAP_WINDOW_DAYS); | ||
| }); | ||
|
|
||
| it("returns default when env is empty string", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = ""; | ||
| expect(resolveWindowDays()).toBe(RECAP_WINDOW_DAYS); | ||
| }); | ||
|
|
||
| it("returns default when env is whitespace", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = " "; | ||
| expect(resolveWindowDays()).toBe(RECAP_WINDOW_DAYS); | ||
| }); | ||
|
|
||
| it("parses a valid integer", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = "14"; | ||
| expect(resolveWindowDays()).toBe(14); | ||
| }); | ||
|
|
||
| it("parses a valid float (fractional days)", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = "3.5"; | ||
| expect(resolveWindowDays()).toBe(3.5); | ||
| }); | ||
|
|
||
| it("returns default for zero", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = "0"; | ||
| expect(resolveWindowDays()).toBe(RECAP_WINDOW_DAYS); | ||
| }); | ||
|
|
||
| it("returns default for negative values", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = "-5"; | ||
| expect(resolveWindowDays()).toBe(RECAP_WINDOW_DAYS); | ||
| }); | ||
|
|
||
| it("returns default for NaN strings", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = "abc"; | ||
| expect(resolveWindowDays()).toBe(RECAP_WINDOW_DAYS); | ||
| }); | ||
|
|
||
| it("returns default for Infinity", () => { | ||
| process.env.AMICODE_SESSION_RECAP_WINDOW_DAYS = "Infinity"; | ||
| expect(resolveWindowDays()).toBe(RECAP_WINDOW_DAYS); | ||
| }); | ||
| }); | ||
|
|
||
| // ── buildRecentSessionsBlock graceful degradation ──────────────────────────── | ||
|
|
||
| describe("buildRecentSessionsBlock — graceful degradation under Node (no bun:sqlite)", () => { | ||
|
|
||
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.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Keep exactly one export in this plugin module.
Line 51 adds another named export in an
opencode-pluginmodule. Rework the module boundary so the runner-facing entry is the only export. KeepresolveWindowDaysprivate or move it outsidepackages/extension/opencode-plugin/. Update tests to verify the supported public entry instead of importing this additional plugin export.As per coding guidelines, "
packages/extension/opencode-plugin/**/*: keep it dependency-free; exactly one export."🤖 Prompt for AI Agents
Source: Coding guidelines