Skip to content
Open
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
29 changes: 16 additions & 13 deletions hindsight-integrations/opencode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,22 @@ Create `~/.hindsight/opencode.json` for persistent configuration:

### Environment Variables

| Variable | Description | Default |
| ----------------------------- | ----------------------------------- | ------------------------------------- |
| `HINDSIGHT_API_URL` | Hindsight API base URL | `https://api.hindsight.vectorize.io` |
| `HINDSIGHT_API_TOKEN` | API key for authentication | (none — required for Hindsight Cloud) |
| `HINDSIGHT_BANK_ID` | Static memory bank ID | `opencode` |
| `HINDSIGHT_AGENT_NAME` | Agent name for dynamic bank IDs | `opencode` |
| `HINDSIGHT_AUTO_RECALL` | Auto-recall on session start | `true` |
| `HINDSIGHT_AUTO_RETAIN` | Auto-retain on session idle | `true` |
| `HINDSIGHT_RETAIN_MODE` | `full-session` or `last-turn` | `full-session` |
| `HINDSIGHT_RECALL_BUDGET` | Recall budget: `low`, `mid`, `high` | `mid` |
| `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens for recall results | `1024` |
| `HINDSIGHT_DYNAMIC_BANK_ID` | Enable dynamic bank ID derivation | `false` |
| `HINDSIGHT_BANK_MISSION` | Bank mission/context | (none) |
| Variable | Description | Default |
| ----------------------------- | -------------------------------------------------------- | ------------------------------------- |
| `HINDSIGHT_API_URL` | Hindsight API base URL | `https://api.hindsight.vectorize.io` |
| `HINDSIGHT_API_TOKEN` | API key for authentication | (none — required for Hindsight Cloud) |
| `HINDSIGHT_BANK_ID` | Static memory bank ID | `opencode` |
| `HINDSIGHT_AGENT_NAME` | Agent name for dynamic bank IDs | `opencode` |
| `HINDSIGHT_AUTO_RECALL` | Auto-recall on session start | `true` |
| `HINDSIGHT_AUTO_RETAIN` | Auto-retain on session idle | `true` |
| `HINDSIGHT_RETAIN_MODE` | `full-session` or `last-turn` | `full-session` |
| `HINDSIGHT_RECALL_BUDGET` | Recall budget: `low`, `mid`, `high` | `mid` |
| `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens for recall results | `1024` |
| `HINDSIGHT_RECALL_TAGS` | Comma-separated, filter recalls | (none) |
| `HINDSIGHT_RECALL_TAGS_MATCH` | Tag match mode: `any`, `all`, `any_strict`, `all_strict` | `any` |
| `HINDSIGHT_RETAIN_TAGS` | Comma-separated, added to every retain | (none) |
| `HINDSIGHT_DYNAMIC_BANK_ID` | Enable dynamic bank ID derivation | `false` |
| `HINDSIGHT_BANK_MISSION` | Bank mission/context | (none) |

> **Debug logging** is a config-only option (`"debug": true` in `opencode.json`
> plugin options or `~/.hindsight/opencode.json`) — there is intentionally no
Expand Down
18 changes: 18 additions & 0 deletions hindsight-integrations/opencode/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ describe("loadConfig", () => {
expect(loadConfig().recallMaxTokens).toBe(1024);
});

it("HINDSIGHT_RETAIN_TAGS parses comma-separated tags", () => {
process.env.HINDSIGHT_RETAIN_TAGS = "user:alice, shared , project-x";
const config = loadConfig();
expect(config.retainTags).toEqual(["user:alice", "shared", "project-x"]);
});

it("HINDSIGHT_RETAIN_TAGS env var overrides plugin option retainTags", () => {
process.env.HINDSIGHT_RETAIN_TAGS = "env-tag";
const config = loadConfig({ retainTags: ["plugin-tag"] });
expect(config.retainTags).toEqual(["env-tag"]);
});

it("HINDSIGHT_RETAIN_TAGS empty string yields empty array", () => {
process.env.HINDSIGHT_RETAIN_TAGS = "";
const config = loadConfig();
expect(config.retainTags).toEqual([]);
});

it("null plugin options are ignored", () => {
const config = loadConfig({ bankId: null, debug: undefined });
expect(config.bankId).toBeNull(); // stays default null
Expand Down
8 changes: 8 additions & 0 deletions hindsight-integrations/opencode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ export function loadConfig(pluginOptions?: Record<string, unknown>): HindsightCo
config["recallTagsMatch"] = recallTagsMatchEnv;
}

const retainTagsEnv = process.env["HINDSIGHT_RETAIN_TAGS"];
if (retainTagsEnv !== undefined) {
config["retainTags"] = retainTagsEnv
.split(",")
.map((t) => t.trim())
.filter(Boolean);
}

const result = config as unknown as HindsightConfig;

// Validate enum-like fields to catch typos early
Expand Down
48 changes: 48 additions & 0 deletions hindsight-integrations/opencode/src/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ describe("event hook — session.idle", () => {
expect(opts.metadata.session_id).toBe("sess-1");
});

it("passes retainTags from config to retain call", async () => {
const client = makeClient();
const messages = [
{ info: { role: "user" }, parts: [{ type: "text", text: "Hello" }] },
{ info: { role: "assistant" }, parts: [{ type: "text", text: "Hi there" }] },
];
const opencodeClient = makeOpencodeClient(messages);
const state = makeState();
const hooks = createHooks(
client,
"bank",
makeConfig({ retainTags: ["user:alice", "shared"], retainEveryNTurns: 1 }),
state,
opencodeClient
);

await hooks.event({
event: { type: "session.idle", properties: { sessionID: "sess-1" } },
});

expect(client.retain).toHaveBeenCalledTimes(1);
const opts = client.retain.mock.calls[0][2];
expect(opts.tags).toEqual(["user:alice", "shared"]);
});

it("skips retain when autoRetain is false", async () => {
const client = makeClient();
const messages = [
Expand Down Expand Up @@ -252,6 +277,29 @@ describe("compacting hook", () => {
expect(opts.metadata.session_id).toBe("sess-1");
});

it("pre-compaction retain passes retainTags from config", async () => {
const client = makeClient();
client.recall.mockResolvedValue({ results: [] });
const messages = [
{ info: { role: "user" }, parts: [{ type: "text", text: "Hello" }] },
{ info: { role: "assistant" }, parts: [{ type: "text", text: "Hi" }] },
];
const output = { context: [] as string[] };
const hooks = createHooks(
client,
"bank",
makeConfig({ retainTags: ["user:alice", "auto-tag"] }),
makeState(),
makeOpencodeClient(messages)
);

await hooks["experimental.session.compacting"]({ sessionID: "sess-1" }, output);

expect(client.retain).toHaveBeenCalledTimes(1);
const opts = client.retain.mock.calls[0][2];
expect(opts.tags).toEqual(["user:alice", "auto-tag"]);
});

it("pre-compaction retain uses chunked documentId in last-turn mode", async () => {
const client = makeClient();
client.recall.mockResolvedValue({ results: [] });
Expand Down