Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,25 @@
### Gotcha

<!-- lore:019c91d6-04af-7334-8374-e8bbf14cb43d -->
* **Calibration used DB message count instead of transformed window count — caused layer 0 false passthrough**: Lore gradient calibration bugs that caused context overflow: (1) Used DB message count instead of transformed window count — after compression, delta saw ~1 new msg → layer 0 passthrough → overflow. Fix: getLastTransformedCount(). (2) actualInput omitted cache.write — cold-cache turns showed ~3 tokens instead of 150K → layer 0. Fix: include cache.write. (3) Trailing pure-text assistant messages cause Anthropic prefill errors, but messages with tool parts must NOT be dropped (SDK converts to tool\_result user-role). Drop predicate: \`hasToolParts\`. (4) Don't mutate message parts you don't own — removed stats PATCH that caused system-reminder persistence bug.

<!-- lore:019cb171-c0ef-7335-9afd-e7874b507b77 -->
* **hostapd -t is not a config dry-run — it adds timestamps to debug output**: hostapd v2.10's \`-t\` flag means 'include timestamps in debug messages', NOT syntax check or dry-run. Running \`hostapd -t \<conf>\` fully initialises the interface and hangs as a running AP. There is no built-in config validation flag in hostapd. For validation, use grep-based checks for known-bad directives (e.g. checking for ieee80211r when it's not compiled in) rather than invoking hostapd itself.

<!-- lore:019c91c0-cdf3-71c9-be52-7f6441fb643e -->
* **Lore plugin only protects projects where it's registered in opencode.json**: The lore gradient transform only runs for projects with lore registered in opencode.json (or globally in ~/.config/opencode/). Projects without it get zero context management — messages accumulate until overflow triggers a stuck compaction loop. This caused a 404K-token overflow in a getsentry/cli session with no opencode.json.
* **Calibration used DB message count instead of transformed window count — caused layer 0 false passthrough**: Lore gradient/context management bugs and fixes: (1) Used DB message count instead of transformed window count — delta ≈ 1 after compression → layer 0 passthrough → overflow. Fix: getLastTransformedCount(). (2) actualInput omitted cache.write — cold-cache showed ~3 tokens → layer 0. Fix: include cache.write. (3) Trailing pure-text assistant messages cause Anthropic prefill errors. Drop loop must run at ALL layers including 0 — at layer 0 result.messages === output.messages (same ref), so pop() trims in place. Messages with tool parts must NOT be dropped (hasToolParts) — dropping causes infinite tool-call loops. (4) Lore only protects projects registered in opencode.json — unregistered projects get zero context management → stuck compaction loops creating orphaned message pairs. Recovery: delete all messages after last good assistant message (has tokens, no error).

<!-- lore:019cb171-c0ea-75cf-bf65-b081373f136b -->
* **mt7921e 3dBm tx power on desktop — disable CLC firmware table**: mt7921e/mt7922 PCIe WiFi cards in desktop PCs (no ACPI SAR tables like WRDS/EWRD) get stuck at ~3 dBm tx power because the CLC (Country Location Code) firmware power lookup falls back to a conservative default when no SAR table exists. Fix: set \`options mt7921\_common disable\_clc=1\` in /etc/modprobe.d/mt7921.conf. This lets the regulatory domain ceiling apply (e.g. 23 dBm on 5GHz ch44 in GB). Also set explicit tx power via \`iw dev \<iface> set txpower fixed 2000\` in ExecStartPost since the module param only takes effect on next module load/reboot.

<!-- lore:019cb171-c0fa-74b0-a9a6-847901efa907 -->
* **Pixel phones fail WPA group key rekey during doze — use 86400s interval**: Android Pixel devices in deep doze/sleep fail to respond to WPA group key handshake frames within hostapd's retry window. With wpa\_group\_rekey=3600, the phone gets deauthenticated every hour ('group key handshake failed (RSN) after 4 tries'). Other devices on the same AP complete the rekey fine. Fix: set wpa\_group\_rekey=86400 (24h) instead of 0 (disabled) for security balance. Also apply to Asus router: nvram set wpa\_gtk\_rekey=86400, wl0\_wpa\_gtk\_rekey=86400, wl1\_wpa\_gtk\_rekey=86400.

<!-- lore:019c91ad-4d47-7afc-90e0-239a9eda57a4 -->
* **Stuck compaction loops leave orphaned user+assistant message pairs in DB**: When OpenCode compaction overflows, it creates paired user+assistant messages per retry (assistant has error.name:'ContextOverflowError', mode:'compaction'). These accumulate and worsen the session. Recovery: find last good assistant message (has tokens, no error), delete all messages after it from both \`message\` and \`part\` tables. Use json\_extract(data, '$.error.name') to identify compaction debris.

<!-- lore:019cb171-c0fe-78a8-a5f8-4ae8e2980a70 -->
* **sudo changes $HOME to /root — hardcode user home in scripts run with sudo**: When running a script with \`sudo\`, \`$HOME\` resolves to \`/root\`, not the invoking user's home. SSH key paths like \`$HOME/.ssh/id\_ed25519\` break. Fix: use \`SUDO\_USER\` env var: \`USER\_HOME=$(eval echo ~${SUDO\_USER:-$USER})\` and reference \`$USER\_HOME/.ssh/id\_ed25519\`. This is a common trap in scripts that need both root privileges (systemctl, writing to /etc) and user-specific resources (SSH keys).

<!-- lore:019c8f4f-67ca-7212-a8c4-8a75b230ceea -->
* **Test DB isolation via LORE\_DB\_PATH and Bun test preload**: Lore test suite uses an isolated temp DB via test/setup.ts preload (bunfig.toml). The preload sets LORE\_DB\_PATH to a mkdtempSync path before any test file imports src/db.ts, and the afterAll cleans up. src/db.ts checks LORE\_DB\_PATH first — if set, uses that exact path instead of ~/.local/share/opencode-lore/lore.db. agents-file.test.ts still needs beforeEach cleanup for intra-file isolation and TEST\_UUIDS cleanup in afterAll (shared explicit UUIDs with ltm.test.ts). Individual test files no longer need close() calls or cross-run cleanup beforeAll blocks — the preload handles DB lifecycle.
* **Test DB isolation via LORE\_DB\_PATH and Bun test preload**: Lore test suite uses isolated temp DB via test/setup.ts preload (bunfig.toml). Preload sets LORE\_DB\_PATH to mkdtempSync path before any imports of src/db.ts; afterAll cleans up. src/db.ts checks LORE\_DB\_PATH first. agents-file.test.ts needs beforeEach cleanup for intra-file isolation and TEST\_UUIDS cleanup in afterAll (shared with ltm.test.ts). Individual test files don't need close() calls preload handles DB lifecycle.

<!-- lore:019cb171-c0f5-741f-96cc-e0862c846202 -->
* **Ubuntu packaged hostapd lacks 802.11r (CONFIG\_IEEE80211R not compiled)**: Ubuntu 24.04's hostapd package (2:2.10-21ubuntu0.x) is compiled without CONFIG\_IEEE80211R. Using \`ieee80211r=1\`, \`mobility\_domain\`, \`ft\_over\_ds\`, \`r0kh\`, \`r1kh\`, or \`FT-PSK\` in wpa\_key\_mgmt causes 'unknown configuration item' errors and hostapd fails to start. 802.11k (rrm\_neighbor\_report, rrm\_beacon\_report) and 802.11v (bss\_transition) ARE compiled in and work. Verify with \`strings /usr/sbin/hostapd | grep ieee80211r\` — absence confirms no FT support. Building from source with CONFIG\_IEEE80211R=y is the only workaround.
* **Ubuntu packaged hostapd lacks 802.11r (CONFIG\_IEEE80211R not compiled)**: Ubuntu 24.04 hostapd (2:2.10-21ubuntu0.x) lacks CONFIG\_IEEE80211R. Using \`ieee80211r=1\`, \`mobility\_domain\`, \`FT-PSK\` etc. causes 'unknown configuration item' and fails to start. 802.11k/v directives ARE compiled in. Verify: \`strings /usr/sbin/hostapd | grep ieee80211r\` — absence confirms no FT support. Build from source with CONFIG\_IEEE80211R=y. Note: hostapd has NO config dry-run flag — \`-t\` just adds timestamps to debug output and fully starts the AP. Use grep-based validation for known-bad directives instead.

<!-- lore:019cb286-7c85-7039-aecf-25781892c9da -->
* **Zod v4 .default({}) no longer applies inner field defaults**: Zod v4 changed \`.default()\` to short-circuit: when input is \`undefined\`, it returns the default value directly without parsing it through inner schema defaults. So \`.object({ enabled: z.boolean().default(true) }).default({})\` returns \`{}\` (no \`enabled\` key), not \`{ enabled: true }\`. Fix: provide fully-populated default objects — \`.default({ enabled: true })\`. This affected all nested config sections in src/config.ts during the v3→v4 upgrade. The import \`import { z } from "zod"\` is unchanged — Zod 4's main entry point is the v4 API.

### Pattern

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"dependencies": {
"remark": "^15.0.1",
"uuidv7": "^1.1.0",
"zod": "^3.25.0"
"zod": "^4.3.6"
},
"devDependencies": {
"@opencode-ai/plugin": "^1.1.39",
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ export const LoreConfig = z.object({
/** Max fraction of usable context reserved for LTM system-prompt injection. Default: 0.10 (10%). */
ltm: z.number().min(0.02).max(0.3).default(0.10),
})
.default({}),
.default({ distilled: 0.25, raw: 0.4, output: 0.25, ltm: 0.10 }),
distillation: z
.object({
minMessages: z.number().min(3).default(8),
maxSegment: z.number().min(5).default(50),
metaThreshold: z.number().min(3).default(10),
})
.default({}),
.default({ minMessages: 8, maxSegment: 50, metaThreshold: 10 }),
knowledge: z
.object({
/** Set to false to disable long-term knowledge storage and system-prompt injection.
Expand All @@ -32,7 +32,7 @@ export const LoreConfig = z.object({
* system prompt. Default: true. */
enabled: z.boolean().default(true),
})
.default({}),
.default({ enabled: true }),
curator: z
.object({
enabled: z.boolean().default(true),
Expand All @@ -41,15 +41,15 @@ export const LoreConfig = z.object({
/** Max knowledge entries per project before consolidation triggers. Default: 25. */
maxEntries: z.number().min(10).default(25),
})
.default({}),
.default({ enabled: true, onIdle: true, afterTurns: 10, maxEntries: 25 }),
pruning: z
.object({
/** Days to keep distilled temporal messages before pruning. Default: 120. */
retention: z.number().min(1).default(120),
/** Max total temporal_messages storage in MB before emergency pruning. Default: 1024 (1 GB). */
maxStorage: z.number().min(50).default(1024),
})
.default({}),
.default({ retention: 120, maxStorage: 1024 }),
crossProject: z.boolean().default(true),
agentsFile: z
.object({
Expand All @@ -58,7 +58,7 @@ export const LoreConfig = z.object({
/** Path to the agents file, relative to the project root. */
path: z.string().default("AGENTS.md"),
})
.default({}),
.default({ enabled: true, path: "AGENTS.md" }),
});

export type LoreConfig = z.infer<typeof LoreConfig>;
Expand Down