@log0u7/opencode-memory 0.2.0 → 0.4.0
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/CHANGELOG.md +104 -1
- package/README.md +36 -5
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +25 -2
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/memory-ops.d.ts +58 -0
- package/dist/lib/memory-ops.d.ts.map +1 -0
- package/dist/lib/memory-ops.js +138 -0
- package/dist/lib/memory-ops.js.map +1 -0
- package/dist/lib/reflection.d.ts +27 -0
- package/dist/lib/reflection.d.ts.map +1 -0
- package/dist/lib/reflection.js +130 -0
- package/dist/lib/reflection.js.map +1 -0
- package/dist/lib/sqlite.d.ts.map +1 -1
- package/dist/lib/sqlite.js +5 -3
- package/dist/lib/sqlite.js.map +1 -1
- package/dist/lib/store.d.ts +49 -2
- package/dist/lib/store.d.ts.map +1 -1
- package/dist/lib/store.js +175 -24
- package/dist/lib/store.js.map +1 -1
- package/dist/plugin-v2.d.ts.map +1 -1
- package/dist/plugin-v2.js +135 -75
- package/dist/plugin-v2.js.map +1 -1
- package/dist/plugin.d.ts +8 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +143 -73
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,110 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.4.0] - 2026-10-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Lesson feedback loop: the system guidance and the compaction prompt now nudge the agent
|
|
13
|
+
to save reusable lessons (`kind=lesson`), and lessons are injected first in later
|
|
14
|
+
sessions' inventory, ahead of recency noise (`store.list` gained an optional `kind`
|
|
15
|
+
filter).
|
|
16
|
+
- Optional LLM reflection at `session.idle`, off by default: with
|
|
17
|
+
`reflection.enabled: true` in the plugin options, the plugin distills at most
|
|
18
|
+
`maxLessons` durable lessons from the bounded session tail with one LLM call per
|
|
19
|
+
session and saves them as lessons. Options: `model` (`"provider/model"`), `maxLessons`
|
|
20
|
+
(3), `transcriptChars` (8000), `timeoutMs` (60000). Subagent sessions are skipped and the
|
|
21
|
+
run is best-effort: failures are silent.
|
|
22
|
+
|
|
23
|
+
- New `memory_stats` tool (both hosts): counts entries by kind for the project scope
|
|
24
|
+
(plus global) with a total, so the model can see how much memory exists, including
|
|
25
|
+
how many lessons reflection has written, without paging through `memory_list`.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `pnpm verify` (and therefore CI) now runs the suite with coverage, so the coverage gate
|
|
30
|
+
the docs promise (90% lines/functions/statements, 80% branches) is actually enforced
|
|
31
|
+
instead of only configured.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- The store database is created with restrictive permissions: the file is `0600`
|
|
36
|
+
(enforced on every open, regardless of the process umask, so databases created earlier
|
|
37
|
+
with a permissive umask are tightened too) and the directory is `0700` when the plugin
|
|
38
|
+
creates it, as `SECURITY.md` already promised.
|
|
39
|
+
- Secret detection now also refuses JWTs, PEM private key blocks, AWS access key ids,
|
|
40
|
+
GitLab personal access tokens, Google API keys, and Slack user/app tokens, in addition
|
|
41
|
+
to the existing OpenAI, GitHub, and Slack bot formats.
|
|
42
|
+
- `memory_forget` by id now deletes across scopes: ids are globally unique, so an exact
|
|
43
|
+
id match is precise anywhere, and closed worktrees stay cleanable from any project.
|
|
44
|
+
Query deletes keep the scope lock (a fuzzy match must never silently hit other
|
|
45
|
+
projects).
|
|
46
|
+
- Reflection no longer permanently skips a session whose first `session.idle` arrived
|
|
47
|
+
before the transcript reached the distillation floor: the session is marked reflected
|
|
48
|
+
only once a run is actually attempted, so a later, longer turn still gets exactly one
|
|
49
|
+
reflection.
|
|
50
|
+
- The README safety-limits note on `memory_forget` now matches the code: query deletes
|
|
51
|
+
stay in scope, id deletes are global.
|
|
52
|
+
- Reflection's LLM call is now bounded by a timeout (`reflection.timeoutMs`, default 60s):
|
|
53
|
+
a hung provider no longer wedges a session forever, and the abort reaches the SDK so the
|
|
54
|
+
v1 host still deletes its ephemeral child session.
|
|
55
|
+
- The v2 event stream now reconnects with a capped backoff after an error instead of
|
|
56
|
+
silently ending the reflection loop for good.
|
|
57
|
+
- After `dispose` (v1) or `cleanup` (v2), memory operations no longer reopen the SQLite
|
|
58
|
+
connection: a late tool call or in-flight reflection returns a readable error instead of
|
|
59
|
+
leaking a connection that nobody closes again.
|
|
60
|
+
|
|
61
|
+
## [0.3.0] - 2026-10-14
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- Deterministic injection: the first request of each session carries a bounded list of the
|
|
66
|
+
project's most recent entries (titles, kinds, dates, no bodies, ~1200 characters worst
|
|
67
|
+
case), so using memory no longer depends on the model remembering to call
|
|
68
|
+
`memory_search`. The tool stays for deeper lookups.
|
|
69
|
+
- Compaction inventory: the compaction context now lists the stored entries next to the
|
|
70
|
+
save instruction, so the model summarises from an inventory instead of recall and stops
|
|
71
|
+
re-saving what is already stored.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- Both plugin entry points (`server` for OpenCode 1, `setup` for OpenCode 2) now delegate
|
|
76
|
+
to one shared core (`src/lib/memory-ops.ts`): tool behaviour, error-to-text mapping,
|
|
77
|
+
caps and rendering live in a single place, and each host is thin SDK glue. No
|
|
78
|
+
behavioural change.
|
|
79
|
+
- The system guidance no longer tells the model to search at task start (the injection
|
|
80
|
+
covers it); the credential warning and both tool mentions stay.
|
|
81
|
+
- CONTRIBUTING.md states the merge-policy rationale instead of a solo-maintainer
|
|
82
|
+
preference: `--no-ff` keeps one merge commit per PR, which reads as the PR list via
|
|
83
|
+
`git log --merges --first-parent main` and is a single atomic revert unit, with a
|
|
84
|
+
five-commit PR ceiling bounding the cost. Documentation only, no package change.
|
|
85
|
+
- `memory_save` rewrites an entry that repeats a scope and title instead of adding a
|
|
86
|
+
duplicate, which stopped the store from growing with every compaction.
|
|
87
|
+
- Saving purges rows past their `expires_at` instead of only hiding them at query time.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- `memory_forget` by query or id could delete entries of other projects: deletes are now
|
|
92
|
+
scoped to the caller's scope (plus `global`), like `memory_search` and `memory_list`.
|
|
93
|
+
- A model-supplied `limit` crashed `memory_search`/`memory_list` (fractional, `NaN`) or
|
|
94
|
+
returned the whole store (`-1`, which SQLite reads as unbounded). Limits are clamped to
|
|
95
|
+
1..50, and tool results are bounded: bodies render as 500-character previews.
|
|
96
|
+
- A credential in an entry `title` or `kind` bypassed the secret guard; the scan now covers
|
|
97
|
+
title, kind, body and tags, and `kind` has a length cap.
|
|
98
|
+
- Search returned nothing for partial queries (`"migration strategy"` found no entry
|
|
99
|
+
containing "migration", and `"migrat"` found nothing): a strict AND match is now retried
|
|
100
|
+
as a prefix OR match, and an empty query returns no results instead of failing.
|
|
101
|
+
- The two plugin entry points defaulted to different scopes for the same project (v1 used
|
|
102
|
+
the session worktree, v2 the plugin directory) and disagreed about trailing slashes.
|
|
103
|
+
Both now derive one canonical scope key.
|
|
104
|
+
- The published package could not be loaded by a Node host: `src/lib/sqlite.ts` called a
|
|
105
|
+
bare `require("node:sqlite")`, which is undefined in an ES module. The Node branch now
|
|
106
|
+
uses `createRequire(import.meta.url)`.
|
|
107
|
+
|
|
108
|
+
### Removed
|
|
109
|
+
|
|
110
|
+
- README advertised an FTS5 "recency boost" that the code never implemented; the actual
|
|
111
|
+
ranking (BM25, newest first on ties) is documented instead.
|
|
9
112
|
|
|
10
113
|
## [0.2.0] - 2026-10-12
|
|
11
114
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
Working memory for [OpenCode](https://opencode.ai): a shared SQLite store that all sessions on a machine read and write, so decisions, facts, and compaction summaries survive session boundaries.
|
|
10
10
|
|
|
11
|
-
Every session gets
|
|
11
|
+
Every session gets five tools (`memory_save`, `memory_search`, `memory_list`, `memory_forget`, `memory_stats`). Entries are scoped per project worktree (plus a `global` scope), searched with SQLite FTS5 (BM25 ranking, strict match first with a prefix fallback). Recent entries are also injected into the prompt: once per session, and again during compaction. 100% local, no cloud, no API keys.
|
|
12
|
+
|
|
13
|
+
## Safety limits
|
|
14
|
+
|
|
15
|
+
- Results are capped: at most 50 entries per call, and each body is rendered as a 500-character preview. A tool result has a bounded size whatever the model asks for.
|
|
16
|
+
- `memory_save` rewrites an entry that repeats a scope and title instead of stacking duplicates, and purges expired rows.
|
|
17
|
+
- `memory_forget` by query only deletes inside the caller's scope (plus the shared `global` scope); by id it deletes the entry wherever it lives, since ids are globally unique.
|
|
18
|
+
- Entries that look like secrets (`sk-…`, `ghp_…`, `xoxb-…`) are refused, in the title, kind, body and tags alike.
|
|
12
19
|
|
|
13
20
|
## Install
|
|
14
21
|
|
|
@@ -41,6 +48,7 @@ memory_save(kind="decision", title="Use WAL mode", body="...", tags=["db"])
|
|
|
41
48
|
memory_search("postgres migration strategy")
|
|
42
49
|
memory_list()
|
|
43
50
|
memory_forget(id="...")
|
|
51
|
+
memory_stats()
|
|
44
52
|
```
|
|
45
53
|
|
|
46
54
|
### Tools
|
|
@@ -48,17 +56,40 @@ memory_forget(id="...")
|
|
|
48
56
|
| Tool | Args | Description |
|
|
49
57
|
|---|---|---|
|
|
50
58
|
| `memory_save` | `kind`, `title`, `body`, `scope?`, `tags?`, `expires_at?` | Save an entry. Default scope is the session worktree. |
|
|
51
|
-
| `memory_search` | `query`, `scope?`, `limit?` |
|
|
59
|
+
| `memory_search` | `query`, `scope?`, `limit?` | FTS5 BM25 search; strict match first, prefix OR fallback when nothing matches. |
|
|
52
60
|
| `memory_list` | `scope?`, `limit?` | Recent entries, newest first. |
|
|
53
61
|
| `memory_forget` | `id` or `query` | Delete one entry by id, or all matching a query. |
|
|
62
|
+
| `memory_stats` | `scope?` | Count entries by kind (plus global) with a total; checks how much memory exists. |
|
|
63
|
+
|
|
64
|
+
Entry `kind` is free-form; conventions that work well: `decision`, `fact`, `preference`, `gotcha`, `lesson`, `compaction`.
|
|
65
|
+
|
|
66
|
+
### Feedback loop
|
|
67
|
+
|
|
68
|
+
The agent is nudged to write memory itself: the system prompt asks for durable decisions, facts, preferences and gotchas as they emerge, and the compaction prompt repeats the instruction next to the store inventory. Reusable lessons (`kind=lesson`: a non-obvious approach that clearly failed or succeeded, and why) are injected first in later sessions, so known mistakes are not retried.
|
|
69
|
+
|
|
70
|
+
Optional LLM reflection, off by default: on `session.idle` the plugin sends the last `transcriptChars` of the session to one bounded LLM call and saves at most `maxLessons` distilled lessons through the same store (the secret guard applies). Subagent sessions are skipped and at most one reflection runs per session.
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"plugin": [
|
|
75
|
+
["@log0u7/opencode-memory", { "reflection": { "enabled": true } }]
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Options: `enabled` (default `false`), `model` (`"provider/model"`, default: the host's model), `maxLessons` (default 3, cap 10), `transcriptChars` (default 8000).
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
Cost per reflection: ~2k input tokens (the bounded transcript) and at most ~300 output tokens, at most once per session: roughly $0.001 with a small model, $0.01 with a frontier model (indicative). The nudges and injections are plain text: they cost nothing.
|
|
56
83
|
|
|
57
84
|
### Notes
|
|
58
85
|
|
|
59
86
|
- All sessions on the machine share one store (WAL mode, concurrent-safe).
|
|
60
|
-
- Entries that look like secrets (`sk-…`, `ghp_…`, `xoxb-…`) are refused.
|
|
61
|
-
-
|
|
87
|
+
- Entries that look like secrets (`sk-…`, `ghp_…`, `xoxb-…`) are refused, in the title, kind, body and tags.
|
|
88
|
+
- Every repeated `memory_save` of the same title in the same scope rewrites the entry, so the store does not grow with duplicates, and expired rows are purged on save.
|
|
89
|
+
- `memory_forget` by query never leaves the scope it is called from, so a project cannot fuzzily delete another project's entries; by id it deletes anywhere (ids are globally unique), which keeps closed worktrees cleanable.
|
|
90
|
+
- On the first request of a session, the most recent entries are injected into the system prompt as a bounded list (titles only), so the model does not have to remember to search. `memory_search` remains the tool for deeper lookups.
|
|
91
|
+
- On session compaction, the plugin lists the stored entries in the compaction context and asks the compaction prompt to emit durable decisions, so the model summarises from an inventory instead of recall.
|
|
92
|
+
- Injections stay bounded (~1200 characters worst case) and nothing is injected, nor any store file created, when memory is unused.
|
|
62
93
|
|
|
63
94
|
## Development
|
|
64
95
|
|
package/dist/lib/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAsCF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CA2BnD"}
|
package/dist/lib/db.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync } from "node:fs";
|
|
1
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { openSqlite } from "./sqlite.js";
|
|
4
4
|
const SCHEMA = `
|
|
@@ -27,15 +27,38 @@ CREATE TRIGGER IF NOT EXISTS entries_au AFTER UPDATE ON entries BEGIN
|
|
|
27
27
|
INSERT INTO entries_fts(entries_fts, rowid, title, body, tags) VALUES ('delete', old.rowid, old.title, old.body, old.tags);
|
|
28
28
|
INSERT INTO entries_fts(rowid, title, body, tags) VALUES (new.rowid, new.title, new.body, new.tags);
|
|
29
29
|
END;
|
|
30
|
+
CREATE INDEX IF NOT EXISTS entries_scope_title ON entries(scope, lower(trim(title)));
|
|
30
31
|
`;
|
|
32
|
+
// SECURITY.md promises restrictive permissions whatever the user's umask is, so the
|
|
33
|
+
// directory and files are mode-enforced explicitly instead of left to the umask.
|
|
34
|
+
// POSIX only: chmodSync on Windows flips a read-only bit, which would corrupt usage.
|
|
35
|
+
// Directories the plugin did not create are left to their owner: the db file is
|
|
36
|
+
// 0600 anyway, so a loose parent only leaks file names.
|
|
37
|
+
const POSIX = process.platform !== "win32";
|
|
31
38
|
export function openMemoryDb(path) {
|
|
32
39
|
if (path !== ":memory:") {
|
|
33
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
34
41
|
}
|
|
35
42
|
const conn = openSqlite(path);
|
|
36
43
|
conn.run("PRAGMA journal_mode = WAL");
|
|
37
44
|
conn.run("PRAGMA busy_timeout = 5000");
|
|
38
45
|
conn.exec(SCHEMA);
|
|
46
|
+
if (POSIX && path !== ":memory:") {
|
|
47
|
+
// Also fixes databases created earlier with a permissive umask.
|
|
48
|
+
chmodSync(path, 0o600);
|
|
49
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
50
|
+
try {
|
|
51
|
+
chmodSync(`${path}${suffix}`, 0o600);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// Only tolerate a sidecar that is not written yet (or already
|
|
55
|
+
// checkpointed away); anything else is a real failure.
|
|
56
|
+
if (error.code !== "ENOENT") {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
39
62
|
return { conn, close: () => conn.close() };
|
|
40
63
|
}
|
|
41
64
|
//# sourceMappingURL=db.js.map
|
package/dist/lib/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAC;AAO1D,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Bd,CAAC;AAEF,oFAAoF;AACpF,iFAAiF;AACjF,qFAAqF;AACrF,gFAAgF;AAChF,wDAAwD;AACxD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AAE3C,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;IACtC,IAAI,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IACvC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElB,IAAI,KAAK,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACjC,gEAAgE;QAChE,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACvB,KAAK,MAAM,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,SAAS,CAAC,GAAG,IAAI,GAAG,MAAM,EAAE,EAAE,KAAK,CAAC,CAAC;YACvC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,8DAA8D;gBAC9D,uDAAuD;gBACvD,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACvD,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { MemoryDb } from "./db.js";
|
|
2
|
+
export type SaveArgs = {
|
|
3
|
+
kind: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
tags?: string[] | undefined;
|
|
7
|
+
expires_at?: number | null | undefined;
|
|
8
|
+
};
|
|
9
|
+
export type SearchArgs = {
|
|
10
|
+
query: string;
|
|
11
|
+
limit?: number | undefined;
|
|
12
|
+
};
|
|
13
|
+
export type ListArgs = {
|
|
14
|
+
limit?: number | undefined;
|
|
15
|
+
};
|
|
16
|
+
export type ForgetArgs = {
|
|
17
|
+
id?: string | undefined;
|
|
18
|
+
query?: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
/** One shared behaviour behind both plugin entry points. Every operation returns the
|
|
21
|
+
* model-facing text directly, so a caller cannot bypass caps or error handling. */
|
|
22
|
+
export type MemoryOps = {
|
|
23
|
+
save: (args: SaveArgs, scope: string) => string;
|
|
24
|
+
search: (args: SearchArgs, scope: string) => string;
|
|
25
|
+
list: (args: ListArgs, scope: string) => string;
|
|
26
|
+
forget: (args: ForgetArgs, scope: string) => string;
|
|
27
|
+
stats: (scope: string) => string;
|
|
28
|
+
};
|
|
29
|
+
export declare function createMemoryOps(getDb: () => MemoryDb): MemoryOps;
|
|
30
|
+
/** How much of the store is summarised into a prompt, worst case. */
|
|
31
|
+
export declare const INVENTORY_MAX_CHARS = 1200;
|
|
32
|
+
/** Tool descriptions shared by both entry points so the two hosts cannot drift. */
|
|
33
|
+
export declare const TOOL_DESCRIPTIONS: {
|
|
34
|
+
readonly save: "Save a durable entry to shared working memory (decisions, facts, preferences, gotchas, lessons). Never store credentials.";
|
|
35
|
+
readonly search: "Search shared working memory (all sessions on this machine). Results include this project's scope and global entries.";
|
|
36
|
+
readonly list: "List recent memory entries for a scope (default: this project), newest first.";
|
|
37
|
+
readonly forget: "Delete memory entries by id (any scope: ids are globally unique), or all entries matching a query. Query deletes stay inside this project's scope (plus global entries); an entry saved under another scope needs its id, or that scope, to be removed.";
|
|
38
|
+
readonly stats: "Count memory entries by kind for this project (plus global entries), with a total. Use it to see how much memory already exists, for example how many lessons reflection has written.";
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Produces at most one injection per session key, and only on the first request where
|
|
42
|
+
* the producer has something: a session that starts before any entry exists still gets
|
|
43
|
+
* its inventory as soon as one is available. Keys are capped so a long-lived host
|
|
44
|
+
* cannot grow the set forever; an absent sessionID (v1 marks it optional) degrades to
|
|
45
|
+
* one injection per plugin instance, never per request.
|
|
46
|
+
*/
|
|
47
|
+
export declare function oncePerSession(): {
|
|
48
|
+
inject: (key: string | undefined, produce: () => string | null) => string | null;
|
|
49
|
+
reset: () => void;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Bounded summary of recent entries, injected into the first request of a session and
|
|
53
|
+
* into the compaction context. Lessons are promoted above recency noise so a stored
|
|
54
|
+
* lesson survives a chatty store. `null` when there is nothing worth injecting, so a
|
|
55
|
+
* caller pushes nothing at all.
|
|
56
|
+
*/
|
|
57
|
+
export declare function memoryInventory(getDb: () => MemoryDb, dbPath: string, scope: string): string | null;
|
|
58
|
+
//# sourceMappingURL=memory-ops.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-ops.d.ts","sourceRoot":"","sources":["../../src/lib/memory-ops.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAKxC,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACxC,CAAC;AACF,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACvE,MAAM,MAAM,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjF;mFACmF;AACnF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpD,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,QAAQ,GAAG,SAAS,CAqDhE;AAED,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAIxC,mFAAmF;AACnF,eAAO,MAAM,iBAAiB;aAC5B,IAAI,EAAE,2HAA2H;aACjI,MAAM,EACJ,uHAAuH;aACzH,IAAI,EAAE,+EAA+E;aACrF,MAAM,EACJ,yPAAyP;aAC3P,KAAK,EACH,uLAAuL;CACjL,CAAC;AAEX;;;;;;GAMG;AACH,wBAAgB,cAAc;kBAGP,MAAM,GAAG,SAAS,WAAW,MAAM,MAAM,GAAG,IAAI,KAAG,MAAM,GAAG,IAAI;;EAgBtF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAyBf"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { forget, list, renderEntries, save, search, SecretDetectedError, stats } from "./store.js";
|
|
3
|
+
export function createMemoryOps(getDb) {
|
|
4
|
+
return {
|
|
5
|
+
save: (args, scope) => run(() => {
|
|
6
|
+
const entry = save(getDb(), {
|
|
7
|
+
scope,
|
|
8
|
+
kind: args.kind,
|
|
9
|
+
title: args.title,
|
|
10
|
+
body: args.body,
|
|
11
|
+
tags: args.tags ?? [],
|
|
12
|
+
expires_at: args.expires_at ?? null,
|
|
13
|
+
});
|
|
14
|
+
return `saved ${entry.id} (${entry.scope})`;
|
|
15
|
+
}),
|
|
16
|
+
search: (args, scope) => run(() => {
|
|
17
|
+
const hits = search(getDb(), {
|
|
18
|
+
query: args.query,
|
|
19
|
+
scope,
|
|
20
|
+
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
21
|
+
});
|
|
22
|
+
return hits.length === 0 ? "no matching memory entries" : renderEntries(hits);
|
|
23
|
+
}),
|
|
24
|
+
list: (args, scope) => run(() => {
|
|
25
|
+
const entries = list(getDb(), {
|
|
26
|
+
scope,
|
|
27
|
+
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
28
|
+
});
|
|
29
|
+
return entries.length === 0 ? "no memory entries" : renderEntries(entries);
|
|
30
|
+
}),
|
|
31
|
+
forget: (args, scope) => run(() => {
|
|
32
|
+
if (args.id !== undefined) {
|
|
33
|
+
const deleted = forget(getDb(), { id: args.id, scope });
|
|
34
|
+
return `deleted ${deleted} entr${deleted === 1 ? "y" : "ies"}`;
|
|
35
|
+
}
|
|
36
|
+
if (args.query !== undefined) {
|
|
37
|
+
const deleted = forget(getDb(), { query: args.query, scope });
|
|
38
|
+
return `deleted ${deleted} entr${deleted === 1 ? "y" : "ies"}`;
|
|
39
|
+
}
|
|
40
|
+
return "provide id or query";
|
|
41
|
+
}),
|
|
42
|
+
stats: (scope) => run(() => {
|
|
43
|
+
const counts = stats(getDb(), { scope });
|
|
44
|
+
if (counts.length === 0) {
|
|
45
|
+
return "no memory entries";
|
|
46
|
+
}
|
|
47
|
+
const total = counts.reduce((sum, row) => sum + row.count, 0);
|
|
48
|
+
return `${counts.map((row) => `${row.kind}: ${row.count}`).join(", ")}, total: ${total}`;
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** How much of the store is summarised into a prompt, worst case. */
|
|
53
|
+
export const INVENTORY_MAX_CHARS = 1200;
|
|
54
|
+
const INVENTORY_HEADER = "Memory already stored for this project (lessons first, then newest):";
|
|
55
|
+
/** Tool descriptions shared by both entry points so the two hosts cannot drift. */
|
|
56
|
+
export const TOOL_DESCRIPTIONS = {
|
|
57
|
+
save: "Save a durable entry to shared working memory (decisions, facts, preferences, gotchas, lessons). Never store credentials.",
|
|
58
|
+
search: "Search shared working memory (all sessions on this machine). Results include this project's scope and global entries.",
|
|
59
|
+
list: "List recent memory entries for a scope (default: this project), newest first.",
|
|
60
|
+
forget: "Delete memory entries by id (any scope: ids are globally unique), or all entries matching a query. Query deletes stay inside this project's scope (plus global entries); an entry saved under another scope needs its id, or that scope, to be removed.",
|
|
61
|
+
stats: "Count memory entries by kind for this project (plus global entries), with a total. Use it to see how much memory already exists, for example how many lessons reflection has written.",
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Produces at most one injection per session key, and only on the first request where
|
|
65
|
+
* the producer has something: a session that starts before any entry exists still gets
|
|
66
|
+
* its inventory as soon as one is available. Keys are capped so a long-lived host
|
|
67
|
+
* cannot grow the set forever; an absent sessionID (v1 marks it optional) degrades to
|
|
68
|
+
* one injection per plugin instance, never per request.
|
|
69
|
+
*/
|
|
70
|
+
export function oncePerSession() {
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const MAX_KEYS = 512;
|
|
73
|
+
const inject = (key, produce) => {
|
|
74
|
+
const k = key ?? "__instance__";
|
|
75
|
+
if (seen.has(k)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const produced = produce();
|
|
79
|
+
if (produced === null) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
seen.add(k);
|
|
83
|
+
if (seen.size > MAX_KEYS) {
|
|
84
|
+
seen.clear();
|
|
85
|
+
}
|
|
86
|
+
return produced;
|
|
87
|
+
};
|
|
88
|
+
return { inject, reset: () => seen.clear() };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Bounded summary of recent entries, injected into the first request of a session and
|
|
92
|
+
* into the compaction context. Lessons are promoted above recency noise so a stored
|
|
93
|
+
* lesson survives a chatty store. `null` when there is nothing worth injecting, so a
|
|
94
|
+
* caller pushes nothing at all.
|
|
95
|
+
*/
|
|
96
|
+
export function memoryInventory(getDb, dbPath, scope) {
|
|
97
|
+
if (!existsSync(dbPath)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const lessons = list(getDb(), { scope, limit: 50, kind: "lesson" });
|
|
101
|
+
const others = list(getDb(), { scope, limit: 50 });
|
|
102
|
+
if (lessons.length === 0 && others.length === 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const seen = new Set(lessons.map((entry) => entry.id));
|
|
106
|
+
const ordered = [...lessons, ...others.filter((entry) => !seen.has(entry.id))];
|
|
107
|
+
const lines = [INVENTORY_HEADER];
|
|
108
|
+
let size = INVENTORY_HEADER.length;
|
|
109
|
+
for (const entry of ordered) {
|
|
110
|
+
const line = `- [${entry.kind}] ${entry.title} (updated ${dayOf(entry.updated_at)})`;
|
|
111
|
+
if (size + line.length + 1 > INVENTORY_MAX_CHARS) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
lines.push(line);
|
|
115
|
+
size += line.length + 1;
|
|
116
|
+
}
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
function dayOf(epochMs) {
|
|
120
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
121
|
+
}
|
|
122
|
+
// Tool results are shown to the model: caught errors become readable text, so a
|
|
123
|
+
// rejected save is guidance instead of a crashed tool call.
|
|
124
|
+
function run(fn) {
|
|
125
|
+
try {
|
|
126
|
+
return fn();
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof SecretDetectedError) {
|
|
130
|
+
return "refused: the content looks like a credential; secrets must never be stored in memory";
|
|
131
|
+
}
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
return `error: ${error.message}`;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=memory-ops.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-ops.js","sourceRoot":"","sources":["../../src/lib/memory-ops.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional end-of-turn reflection: distills durable lessons from a session tail with
|
|
3
|
+
* one bounded LLM call and saves them as `lesson` entries. Best-effort by design:
|
|
4
|
+
* a disabled config wires nothing, a failing run is swallowed, and each session is
|
|
5
|
+
* reflected at most once.
|
|
6
|
+
*/
|
|
7
|
+
export type ReflectionConfig = {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
model?: string | undefined;
|
|
10
|
+
maxLessons: number;
|
|
11
|
+
transcriptChars: number;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
};
|
|
14
|
+
export type ReflectionDeps = {
|
|
15
|
+
isSubagent: (sessionID: string) => Promise<boolean>;
|
|
16
|
+
transcript: (sessionID: string) => Promise<string>;
|
|
17
|
+
generate: (prompt: string, sessionID: string, signal: AbortSignal) => Promise<string>;
|
|
18
|
+
saveLesson: (title: string, body: string) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function parseReflectionConfig(raw: unknown): ReflectionConfig | null;
|
|
21
|
+
export declare function distillPrompt(transcript: string, maxLessons: number): string;
|
|
22
|
+
export declare function parseLessons(text: string, max: number): Array<{
|
|
23
|
+
title: string;
|
|
24
|
+
body: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function createReflection(config: ReflectionConfig, deps: ReflectionDeps): (sessionID: string | undefined) => void;
|
|
27
|
+
//# sourceMappingURL=reflection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reflection.d.ts","sourceRoot":"","sources":["../../src/lib/reflection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtF,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnD,CAAC;AAcF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAyB3E;AASD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAU5E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA8B9F;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,cAAc,GACnB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAoDzC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional end-of-turn reflection: distills durable lessons from a session tail with
|
|
3
|
+
* one bounded LLM call and saves them as `lesson` entries. Best-effort by design:
|
|
4
|
+
* a disabled config wires nothing, a failing run is swallowed, and each session is
|
|
5
|
+
* reflected at most once.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_LESSONS_DEFAULT = 3;
|
|
8
|
+
const MAX_LESSONS_CAP = 10;
|
|
9
|
+
const TRANSCRIPT_CHARS_DEFAULT = 8000;
|
|
10
|
+
const TRANSCRIPT_CHARS_MIN = 500;
|
|
11
|
+
const TRANSCRIPT_CHARS_MAX = 50000;
|
|
12
|
+
const TIMEOUT_MS_DEFAULT = 60000;
|
|
13
|
+
const TIMEOUT_MS_MIN = 1000;
|
|
14
|
+
const TIMEOUT_MS_MAX = 600000;
|
|
15
|
+
/** Below this much transcript there is nothing to distill; saves the LLM call. */
|
|
16
|
+
const TRANSCRIPT_MIN_CHARS = 256;
|
|
17
|
+
const MAX_TRACKED = 512;
|
|
18
|
+
export function parseReflectionConfig(raw) {
|
|
19
|
+
if (raw === null || typeof raw !== "object" || raw.enabled !== true) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const rawConfig = raw;
|
|
23
|
+
return {
|
|
24
|
+
enabled: true,
|
|
25
|
+
model: typeof rawConfig.model === "string" && rawConfig.model.includes("/")
|
|
26
|
+
? rawConfig.model
|
|
27
|
+
: undefined,
|
|
28
|
+
maxLessons: clamp(rawConfig.maxLessons, 1, MAX_LESSONS_CAP, MAX_LESSONS_DEFAULT),
|
|
29
|
+
transcriptChars: clamp(rawConfig.transcriptChars, TRANSCRIPT_CHARS_MIN, TRANSCRIPT_CHARS_MAX, TRANSCRIPT_CHARS_DEFAULT),
|
|
30
|
+
timeoutMs: clamp(rawConfig.timeoutMs, TIMEOUT_MS_MIN, TIMEOUT_MS_MAX, TIMEOUT_MS_DEFAULT),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function clamp(value, min, max, fallback) {
|
|
34
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
38
|
+
}
|
|
39
|
+
export function distillPrompt(transcript, maxLessons) {
|
|
40
|
+
return (`You are the persistent-memory reflection for this project's OpenCode sessions. Below is the tail of one ` +
|
|
41
|
+
`coding session transcript. Extract at most ${maxLessons} durable, reusable lessons: non-obvious approaches ` +
|
|
42
|
+
`that clearly failed and why, non-obvious approaches that clearly worked and why, or constraints discovered ` +
|
|
43
|
+
`the hard way. Exclude anything trivially re-derivable from the code, anything session-specific, and any ` +
|
|
44
|
+
`credential. Reply with ONLY a JSON array, no markdown fences and no prose: ` +
|
|
45
|
+
`[{"title": "max 120 chars", "body": "the lesson, max 8000 chars"}]. Reply [] when nothing qualifies.\n\n` +
|
|
46
|
+
`SESSION TRANSCRIPT:\n${transcript}`);
|
|
47
|
+
}
|
|
48
|
+
export function parseLessons(text, max) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(text);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
const fenced = text
|
|
55
|
+
.replace(/^```(?:json)?/m, "")
|
|
56
|
+
.replace(/```$/m, "")
|
|
57
|
+
.trim();
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(fenced);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(parsed)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const lessons = [];
|
|
69
|
+
for (const item of parsed.slice(0, max)) {
|
|
70
|
+
if (item === null || typeof item !== "object") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const { title, body } = item;
|
|
74
|
+
if (typeof title !== "string" || typeof body !== "string" || !title.trim() || !body.trim()) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
lessons.push({ title: title.slice(0, 120), body: body.slice(0, 8000) });
|
|
78
|
+
}
|
|
79
|
+
return lessons;
|
|
80
|
+
}
|
|
81
|
+
export function createReflection(config, deps) {
|
|
82
|
+
const done = new Set();
|
|
83
|
+
const inFlight = new Set();
|
|
84
|
+
const run = async (sessionID) => {
|
|
85
|
+
// Only a session we actually tried to reflect is marked done. A too-short
|
|
86
|
+
// transcript leaves it unsettled, so a later, longer idle can still reflect it
|
|
87
|
+
// once: idle fires at the end of every turn, not once per session.
|
|
88
|
+
let settled = false;
|
|
89
|
+
try {
|
|
90
|
+
if (await deps.isSubagent(sessionID)) {
|
|
91
|
+
settled = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const tail = (await deps.transcript(sessionID)).slice(-config.transcriptChars);
|
|
95
|
+
if (tail.length < Math.min(TRANSCRIPT_MIN_CHARS, config.transcriptChars)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
settled = true;
|
|
99
|
+
// Bound the LLM call: a hung provider must not wedge a session forever. The
|
|
100
|
+
// hosts forward this signal to the SDK, so the abort also unblocks their own
|
|
101
|
+
// cleanup (the v1 host deletes the ephemeral child session in its `finally`).
|
|
102
|
+
const reply = await deps.generate(distillPrompt(tail, config.maxLessons), sessionID, AbortSignal.timeout(config.timeoutMs));
|
|
103
|
+
for (const lesson of parseLessons(reply, config.maxLessons)) {
|
|
104
|
+
deps.saveLesson(lesson.title, lesson.body);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Reflection is best-effort: never surface a failure to the host. A failed
|
|
109
|
+
// attempt still settles the session so a broken pipeline is not retried forever.
|
|
110
|
+
settled = true;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
inFlight.delete(sessionID);
|
|
114
|
+
if (settled) {
|
|
115
|
+
done.add(sessionID);
|
|
116
|
+
if (done.size > MAX_TRACKED) {
|
|
117
|
+
done.clear();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return (sessionID) => {
|
|
123
|
+
if (!sessionID || !config.enabled || done.has(sessionID) || inFlight.has(sessionID)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
inFlight.add(sessionID);
|
|
127
|
+
void run(sessionID);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=reflection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reflection.js","sourceRoot":"","sources":["../../src/lib/reflection.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/sqlite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/lib/sqlite.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/lib/sqlite.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC;IACvD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IAC5D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,IAAI,IAAI,CAAC;CACf;AAqED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAanD"}
|