dsh-context-memory 0.1.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 +21 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/cordis.patch.yml +6 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +3 -0
- package/lib/cli.d.ts +9 -0
- package/lib/cli.js +128 -0
- package/lib/client/index.d.ts +13 -0
- package/lib/client/index.js +32 -0
- package/lib/client/view.d.ts +20 -0
- package/lib/client/view.js +64 -0
- package/lib/guard.d.ts +66 -0
- package/lib/guard.js +139 -0
- package/lib/identity.d.ts +36 -0
- package/lib/identity.js +56 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +28 -0
- package/lib/memory.d.ts +92 -0
- package/lib/memory.js +200 -0
- package/lib/memory.web.js +60 -0
- package/lib/recall.d.ts +40 -0
- package/lib/recall.js +77 -0
- package/lib/routes.d.ts +14 -0
- package/lib/routes.js +41 -0
- package/lib/store.d.ts +30 -0
- package/lib/store.js +62 -0
- package/lib/types.d.ts +114 -0
- package/lib/types.js +12 -0
- package/package.json +45 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- **Remember** — append-only `.memory/events.jsonl`; every memory carries provenance
|
|
8
|
+
(session, step, author) so the question "why does the agent think it knows this" has
|
|
9
|
+
an answer.
|
|
10
|
+
- **Recall** — relevance-ranked retrieval (token overlap, recency, verification
|
|
11
|
+
strength) instead of dumping every stored memory into context.
|
|
12
|
+
- **Forget with a reason** — a memory is superseded, never silently deleted, and the
|
|
13
|
+
supersede event keeps the reason on the record.
|
|
14
|
+
- **Conflict detection** — two active memories about the same subject are reported as a
|
|
15
|
+
conflict; the plugin does not pick a winner for you.
|
|
16
|
+
- **Verification** — a checkable memory is compiled into a platform-aware guard and
|
|
17
|
+
re-checked on demand. `dsh-context-memory verify` exits non-zero only for a memory
|
|
18
|
+
whose guard ran and failed; a guard that cannot run is reported separately and is
|
|
19
|
+
never counted as a broken memory.
|
|
20
|
+
- **Panel** — a `Context memory` settings page listing memory, verification status,
|
|
21
|
+
conflicts, and provenance.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hj01857655
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# dsh-context-memory
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-context-memory) [](https://github.com/hj01857655/dsh-context-memory/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Cross-session memory for your dsh agent — remembered, recalled, and re-verified instead of assumed.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
dsh plugin --profile web add dsh-context-memory
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
Every session with your agent starts from nothing. What you decided yesterday, which
|
|
16
|
+
approach failed, the constraint you explained once — all of it is gone, and you explain
|
|
17
|
+
it again.
|
|
18
|
+
|
|
19
|
+
This plugin keeps it, and — unlike a note file — re-checks the parts that can be checked.
|
|
20
|
+
|
|
21
|
+
- **Remember.** A memory carries provenance: which session, which step, whether it came
|
|
22
|
+
from you, the agent, or a plugin. "Why does the agent think it knows this" has an
|
|
23
|
+
answer.
|
|
24
|
+
- **Recall.** Retrieval is ranked (token overlap, recency, verification strength) instead
|
|
25
|
+
of pasting every memory into the context window. Each hit reports the breakdown of its
|
|
26
|
+
score, so a ranking is never something you have to trust.
|
|
27
|
+
- **Forget with a reason.** A memory is superseded, never deleted. The wrong memories are
|
|
28
|
+
the most valuable records in the log — they are the only thing that can explain a past
|
|
29
|
+
decision.
|
|
30
|
+
- **Conflict detection.** Two active memories about the same subject are reported as a
|
|
31
|
+
conflict. The plugin does not pick a winner; the log does not say which one is right,
|
|
32
|
+
and guessing would be the unfounded assertion this plugin exists to remove.
|
|
33
|
+
- **Verification.** A memory whose statement has a checkable shape is compiled into a
|
|
34
|
+
platform-aware guard and re-checked on demand. This is the part a notes file cannot do.
|
|
35
|
+
|
|
36
|
+
## The point
|
|
37
|
+
|
|
38
|
+
A memory is a claim about the world. Storing it makes it *available*; it does not make it
|
|
39
|
+
*true*. Statements like "the file README.md exists" or "config.json contains pnpm" were
|
|
40
|
+
true when you said them and stop being true quietly.
|
|
41
|
+
|
|
42
|
+
So `verify` turns each checkable memory back into a check and runs it:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
dsh-context-memory verify
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
passed 4f2a9c1b8e30 the file package.json exists
|
|
50
|
+
violated 7d1e0a55c9f2 the file README.md contains Installation
|
|
51
|
+
uncheckable 2b8f31aa0456 we settled on pnpm because yarn hoisting broke the build
|
|
52
|
+
broken a91c4e77b0d3 the file scripts/release.sh exists
|
|
53
|
+
|
|
54
|
+
passed 1, violated 1, broken 1, uncheckable 1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Four outcomes, and the distinction between them is the whole point:
|
|
58
|
+
|
|
59
|
+
| Outcome | Meaning |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `passed` | The check ran and the memory holds. |
|
|
62
|
+
| `violated` | The check ran and the world disagrees with the memory. |
|
|
63
|
+
| `broken` | The check itself could not run — a typo, a missing binary. **Not** evidence about the memory. |
|
|
64
|
+
| `uncheckable` | No check could be compiled. The memory is still stored and recalled; it is just not verified. |
|
|
65
|
+
|
|
66
|
+
`verify` exits non-zero **only** for `violated`. A broken guard is a defect in the check,
|
|
67
|
+
and letting it turn a pipeline red is how a typo would get a true memory marked false —
|
|
68
|
+
after which you learn to ignore the output.
|
|
69
|
+
|
|
70
|
+
Nothing is ever marked as verified because it was stored, and a memory supplied by the
|
|
71
|
+
agent is never executed as a command: guards come from templates that only ever
|
|
72
|
+
interpolate quoted file paths and literal search strings.
|
|
73
|
+
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
dsh-context-memory remember "the file package.json exists"
|
|
78
|
+
dsh-context-memory list
|
|
79
|
+
dsh-context-memory recall "how do we install"
|
|
80
|
+
dsh-context-memory conflicts
|
|
81
|
+
dsh-context-memory forget 4f2a9c1b8e30 "moved to bun"
|
|
82
|
+
dsh-context-memory verify # exit 1 only on a real violation
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Panel
|
|
86
|
+
|
|
87
|
+
A `Context memory` page inside Settings: active and superseded memories, verification
|
|
88
|
+
status per memory, conflicts, and provenance. The host registers
|
|
89
|
+
`GET /api/context-memory.panel` and rebuilds the payload from the log on every request,
|
|
90
|
+
so the panel shows what is on disk rather than what was in memory at startup. A headless
|
|
91
|
+
host has no web connection and simply skips the route.
|
|
92
|
+
|
|
93
|
+
## Layout
|
|
94
|
+
|
|
95
|
+
| Path | Role |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `src/index.ts` | Host half — `apply(ctx)`, exposes the `contextMemory` service |
|
|
98
|
+
| `src/memory.ts` | Fold the log, remember, supersede, conflicts, verify |
|
|
99
|
+
| `src/store.ts` | Append-only `.memory/events.jsonl` |
|
|
100
|
+
| `src/guard.ts` | Statement → platform-aware check; run it; classify pass / violated / broken |
|
|
101
|
+
| `src/recall.ts` | Relevance ranking with an explainable score |
|
|
102
|
+
| `src/identity.ts` | Id derived from the normalised subject, so repeats fold |
|
|
103
|
+
| `src/cli.ts` / `src/bin.ts` | Command-line entry, usable without dsh |
|
|
104
|
+
| `src/routes.ts` | Panel and verify routes on the host's web connection |
|
|
105
|
+
| `src/client/view.tsx` | Browser half — pure rendering, statically tested in Node |
|
|
106
|
+
| `src/client/index.tsx` | Browser half — fetch and settings registration |
|
|
107
|
+
|
|
108
|
+
## Why the log is append-only
|
|
109
|
+
|
|
110
|
+
Current state is always a fold over the events, recomputed on each read. The
|
|
111
|
+
alternative — a mutable store — lets the last writer decide what was always true, and
|
|
112
|
+
"what did the agent believe on Tuesday, and why" becomes unanswerable. The log is the
|
|
113
|
+
evidence, so a torn trailing line after a crash is dropped (recoverable) while damage
|
|
114
|
+
anywhere else is reported rather than skipped.
|
|
115
|
+
|
|
116
|
+
## Develop
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
npm ci
|
|
120
|
+
npm run typecheck
|
|
121
|
+
npm run build
|
|
122
|
+
npm test
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# The host half loads through this patch. `insert` appends the plugin to the profile's
|
|
2
|
+
# bundle graph; the id must be unique within the profile or the loader reports a
|
|
3
|
+
# duplicate entry id and refuses to start.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: dsh-context-memory
|
|
6
|
+
name: 'dsh-context-memory'
|
package/lib/bin.d.ts
ADDED
package/lib/bin.js
ADDED
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-line entry.
|
|
3
|
+
*
|
|
4
|
+
* The same modules the host uses, reachable without booting dsh — so what the plugin
|
|
5
|
+
* claims about a project can be checked independently, and `verify` can run in CI.
|
|
6
|
+
*
|
|
7
|
+
* @module context-memory/cli
|
|
8
|
+
*/
|
|
9
|
+
export declare function run(argv: string[]): number;
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-line entry.
|
|
3
|
+
*
|
|
4
|
+
* The same modules the host uses, reachable without booting dsh — so what the plugin
|
|
5
|
+
* claims about a project can be checked independently, and `verify` can run in CI.
|
|
6
|
+
*
|
|
7
|
+
* @module context-memory/cli
|
|
8
|
+
*/
|
|
9
|
+
import { parseArgs } from 'node:util';
|
|
10
|
+
import { ContextMemory } from './memory.js';
|
|
11
|
+
export function run(argv) {
|
|
12
|
+
const { positionals, values } = parseArgs({
|
|
13
|
+
args: argv,
|
|
14
|
+
allowPositionals: true,
|
|
15
|
+
options: {
|
|
16
|
+
subject: { type: 'string' },
|
|
17
|
+
kind: { type: 'string' },
|
|
18
|
+
source: { type: 'string' },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const memory = new ContextMemory(process.cwd());
|
|
22
|
+
const cmd = positionals[0] ?? 'list';
|
|
23
|
+
switch (cmd) {
|
|
24
|
+
case 'remember': {
|
|
25
|
+
const text = positionals.slice(1).join(' ');
|
|
26
|
+
if (text.trim().length === 0) {
|
|
27
|
+
console.log('Usage: dsh-context-memory remember "<statement>" [--subject <what it is about>]');
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
// `--subject` is what makes a conflict expressible from the command line: two
|
|
31
|
+
// different statements about one thing stay separate records instead of folding.
|
|
32
|
+
const stored = memory.remember({
|
|
33
|
+
text,
|
|
34
|
+
...(values['subject'] !== undefined ? { subject: String(values['subject']) } : {}),
|
|
35
|
+
...(values['kind'] !== undefined ? { kind: String(values['kind']) } : {}),
|
|
36
|
+
...(values['source'] !== undefined ? { source: String(values['source']) } : {}),
|
|
37
|
+
});
|
|
38
|
+
const guard = stored.guard ? ` (guard: ${stored.guard.command})` : ' (no check could be compiled)';
|
|
39
|
+
console.log(`Remembered ${stored.id}: ${stored.text}${guard}`);
|
|
40
|
+
// A memory with no executable check is recorded, and the caller is told plainly
|
|
41
|
+
// that nothing will verify it — rather than reporting a success that implies more.
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
case 'list': {
|
|
45
|
+
const active = memory.active();
|
|
46
|
+
if (active.length === 0) {
|
|
47
|
+
console.log('No memories recorded.');
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
for (const item of active) {
|
|
51
|
+
const mark = item.verification?.outcome === 'passed' ? '✓' : item.verification ? '✗' : '·';
|
|
52
|
+
console.log(`${mark} ${item.id} ${item.text}`);
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
case 'recall': {
|
|
57
|
+
const query = positionals.slice(1).join(' ');
|
|
58
|
+
if (query.trim().length === 0) {
|
|
59
|
+
console.log('Usage: dsh-context-memory recall "<query>"');
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const hits = memory.recall(query);
|
|
63
|
+
if (hits.length === 0) {
|
|
64
|
+
console.log('Nothing relevant is remembered.');
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
for (const hit of hits) {
|
|
68
|
+
console.log(`${hit.score.toFixed(3)} ${hit.memory.text}`);
|
|
69
|
+
console.log(` ${hit.why}`);
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
case 'forget': {
|
|
74
|
+
const id = positionals[1] ?? '';
|
|
75
|
+
const reason = positionals.slice(2).join(' ');
|
|
76
|
+
if (id.length === 0 || reason.trim().length === 0) {
|
|
77
|
+
console.log('Usage: dsh-context-memory forget <id> "<reason>"');
|
|
78
|
+
console.log('A reason is required: a superseded memory is kept as the record of why it stopped being true.');
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const ok = memory.forget(id, reason);
|
|
82
|
+
console.log(ok ? `Superseded ${id}: ${reason}` : `No memory with id ${id}`);
|
|
83
|
+
return ok ? 0 : 1;
|
|
84
|
+
}
|
|
85
|
+
case 'conflicts': {
|
|
86
|
+
const conflicts = memory.conflicts();
|
|
87
|
+
if (conflicts.length === 0) {
|
|
88
|
+
console.log('No conflicts.');
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
for (const conflict of conflicts) {
|
|
92
|
+
console.log(`Subject: ${conflict.subject}`);
|
|
93
|
+
for (const item of conflict.memories)
|
|
94
|
+
console.log(` - ${item.id}: ${item.text}`);
|
|
95
|
+
}
|
|
96
|
+
// A conflict is a question for the user, not a failure of this command.
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
case 'verify': {
|
|
100
|
+
const summary = memory.verify();
|
|
101
|
+
for (const result of summary.results) {
|
|
102
|
+
console.log(`${result.outcome.padEnd(11)} ${result.id} ${result.text}`);
|
|
103
|
+
if (result.outcome === 'broken' && result.output.trim().length > 0) {
|
|
104
|
+
console.log(` the check could not run: ${result.output.trim().split('\n')[0]}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log(`\npassed ${summary.passed}, violated ${summary.violated}, broken ${summary.broken}, uncheckable ${summary.uncheckable}`);
|
|
108
|
+
if (summary.broken > 0) {
|
|
109
|
+
console.log('broken guards are a defect in the check, not a memory that is wrong — they do not fail this command');
|
|
110
|
+
}
|
|
111
|
+
return ContextMemory.exitCode(summary);
|
|
112
|
+
}
|
|
113
|
+
case 'help':
|
|
114
|
+
default:
|
|
115
|
+
console.log('Usage: dsh-context-memory <command>');
|
|
116
|
+
console.log('Commands:');
|
|
117
|
+
console.log(' remember "<statement>" record a memory and compile a check if possible');
|
|
118
|
+
console.log(' --subject <what it is about> make a later disagreeing statement a conflict');
|
|
119
|
+
console.log(' --kind <fact|decision|preference|pitfall>');
|
|
120
|
+
console.log(' --source <user|agent|plugin>');
|
|
121
|
+
console.log(' list active memories, with verification status');
|
|
122
|
+
console.log(' recall "<query>" rank memories by relevance');
|
|
123
|
+
console.log(' forget <id> "<reason>" retire a memory, keeping the reason');
|
|
124
|
+
console.log(' conflicts active memories that disagree');
|
|
125
|
+
console.log(' verify re-run every compiled check; exit 1 only on a real violation');
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-context-memory — the registration shell.
|
|
3
|
+
*
|
|
4
|
+
* Declaring `inject` at module scope is required, not stylistic: without it the client
|
|
5
|
+
* loader cannot resolve the settings and connection services, and the half fails to
|
|
6
|
+
* register with an unhelpful error.
|
|
7
|
+
*
|
|
8
|
+
* @module context-memory/client
|
|
9
|
+
*/
|
|
10
|
+
export declare const inject: string[];
|
|
11
|
+
export declare function apply(ctx: {
|
|
12
|
+
inject: (deps: string[], fn: (...services: unknown[]) => void) => void;
|
|
13
|
+
}): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-context-memory — the registration shell.
|
|
3
|
+
*
|
|
4
|
+
* Declaring `inject` at module scope is required, not stylistic: without it the client
|
|
5
|
+
* loader cannot resolve the settings and connection services, and the half fails to
|
|
6
|
+
* register with an unhelpful error.
|
|
7
|
+
*
|
|
8
|
+
* @module context-memory/client
|
|
9
|
+
*/
|
|
10
|
+
import { renderPanel } from './view.js';
|
|
11
|
+
export const inject = ['@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-client-connection'];
|
|
12
|
+
export function apply(ctx) {
|
|
13
|
+
ctx.inject(inject, (settings, connection) => {
|
|
14
|
+
const s = settings;
|
|
15
|
+
const c = connection;
|
|
16
|
+
s.section('context-memory', {
|
|
17
|
+
title: 'Context memory',
|
|
18
|
+
render: async () => {
|
|
19
|
+
try {
|
|
20
|
+
const res = await c.fetch('/api/context-memory.panel');
|
|
21
|
+
const payload = await res.json();
|
|
22
|
+
return renderPanel(payload);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// Say what failed rather than rendering an empty panel, which would read as
|
|
26
|
+
// "nothing is remembered" — a different and wrong answer.
|
|
27
|
+
return `<div class="context-memory-panel"><h2>Context memory</h2><p>Could not load memories: ${String(error)}</p></div>`;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-context-memory — the view model.
|
|
3
|
+
*
|
|
4
|
+
* Rendering is kept here, separate from the fetch-and-register shell, so it can be
|
|
5
|
+
* statically rendered in Node during tests. The panel is a claim about what the agent
|
|
6
|
+
* remembers, so it is worth asserting on without a browser.
|
|
7
|
+
*
|
|
8
|
+
* @module context-memory/client/view
|
|
9
|
+
*/
|
|
10
|
+
import type { MemoryState, PanelPayload } from '../types.js';
|
|
11
|
+
/**
|
|
12
|
+
* A memory's verification badge.
|
|
13
|
+
*
|
|
14
|
+
* Four states, not two, because "no check exists" and "the check could not run" are
|
|
15
|
+
* different answers from "the check failed" — collapsing them would report an
|
|
16
|
+
* unverifiable memory and a false one identically, which is the failure this plugin
|
|
17
|
+
* exists to avoid.
|
|
18
|
+
*/
|
|
19
|
+
export declare function verificationLabel(memory: MemoryState): string;
|
|
20
|
+
export declare function renderPanel(payload: PanelPayload): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-context-memory — the view model.
|
|
3
|
+
*
|
|
4
|
+
* Rendering is kept here, separate from the fetch-and-register shell, so it can be
|
|
5
|
+
* statically rendered in Node during tests. The panel is a claim about what the agent
|
|
6
|
+
* remembers, so it is worth asserting on without a browser.
|
|
7
|
+
*
|
|
8
|
+
* @module context-memory/client/view
|
|
9
|
+
*/
|
|
10
|
+
function escapeHtml(value) {
|
|
11
|
+
return value
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>')
|
|
15
|
+
.replace(/"/g, '"');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A memory's verification badge.
|
|
19
|
+
*
|
|
20
|
+
* Four states, not two, because "no check exists" and "the check could not run" are
|
|
21
|
+
* different answers from "the check failed" — collapsing them would report an
|
|
22
|
+
* unverifiable memory and a false one identically, which is the failure this plugin
|
|
23
|
+
* exists to avoid.
|
|
24
|
+
*/
|
|
25
|
+
export function verificationLabel(memory) {
|
|
26
|
+
if (!memory.guard)
|
|
27
|
+
return 'no check';
|
|
28
|
+
switch (memory.verification?.outcome) {
|
|
29
|
+
case 'passed': return 'checked, holds';
|
|
30
|
+
case 'violated': return 'checked, does not hold';
|
|
31
|
+
case 'broken': return 'check unusable';
|
|
32
|
+
default: return 'not yet checked';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function rows(memories) {
|
|
36
|
+
return memories
|
|
37
|
+
.map((memory) => {
|
|
38
|
+
const provenance = [
|
|
39
|
+
memory.source,
|
|
40
|
+
memory.sessionId ? `session ${memory.sessionId}` : null,
|
|
41
|
+
memory.step !== undefined ? `step ${memory.step}` : null,
|
|
42
|
+
].filter(Boolean).join(' · ');
|
|
43
|
+
return `<tr><td><code>${escapeHtml(memory.id)}</code></td><td>${escapeHtml(memory.text)}</td><td>${escapeHtml(memory.kind)}</td><td>${escapeHtml(verificationLabel(memory))}</td><td>${escapeHtml(provenance)}</td></tr>`;
|
|
44
|
+
})
|
|
45
|
+
.join('');
|
|
46
|
+
}
|
|
47
|
+
export function renderPanel(payload) {
|
|
48
|
+
const { totals } = payload;
|
|
49
|
+
const summary = `<p class="memory-summary">${totals.active} active (${totals.verified} checked and holding, ${totals.unverified} not), ${totals.superseded} superseded, ${totals.withGuard} with a compiled check.</p>`;
|
|
50
|
+
const conflictBlock = payload.conflicts.length === 0
|
|
51
|
+
? '<p>No conflicts.</p>'
|
|
52
|
+
: `<div class="memory-conflicts"><h3>Conflicts</h3><ul>${payload.conflicts
|
|
53
|
+
.map((conflict) => `<li><strong>${escapeHtml(conflict.subject)}</strong><ul>${conflict.memories
|
|
54
|
+
.map((memory) => `<li><code>${escapeHtml(memory.id)}</code> ${escapeHtml(memory.text)}</li>`)
|
|
55
|
+
.join('')}</ul></li>`)
|
|
56
|
+
.join('')}</ul></div>`;
|
|
57
|
+
const activeBlock = payload.active.length === 0
|
|
58
|
+
? '<p>Nothing is remembered yet.</p>'
|
|
59
|
+
: `<table><thead><tr><th>Id</th><th>Memory</th><th>Kind</th><th>Status</th><th>From</th></tr></thead><tbody>${rows(payload.active)}</tbody></table>`;
|
|
60
|
+
const supersededBlock = payload.superseded.length === 0
|
|
61
|
+
? ''
|
|
62
|
+
: `<details><summary>Superseded (${payload.superseded.length})</summary><table><thead><tr><th>Id</th><th>Memory</th><th>Kind</th><th>Status</th><th>From</th></tr></thead><tbody>${rows(payload.superseded)}</tbody></table></details>`;
|
|
63
|
+
return `<div class="context-memory-panel"><h2>Context memory</h2>${summary}${conflictBlock}<h3>Active</h3>${activeBlock}${supersededBlock}</div>`;
|
|
64
|
+
}
|
package/lib/guard.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard compilation and execution for memories.
|
|
3
|
+
*
|
|
4
|
+
* A memory is a claim, and this module is what decides whether the claim still holds.
|
|
5
|
+
* It keeps the same distinction dsh-verdict does, for the same reason: **a check that
|
|
6
|
+
* cannot run is not a memory that is wrong.** Conflating them would mean a mistyped
|
|
7
|
+
* path in a guard marks a perfectly good memory as false, and the user learns to
|
|
8
|
+
* ignore the output.
|
|
9
|
+
*
|
|
10
|
+
* Recognition is narrow on purpose. Only statement shapes whose checkable predicate is
|
|
11
|
+
* unambiguous get a guard; everything else stays uncheckable and says so. Emitting an
|
|
12
|
+
* approximate command would produce a check that exits 0 while verifying nothing, and
|
|
13
|
+
* every later "verified" would be a lie.
|
|
14
|
+
*
|
|
15
|
+
* There is deliberately **no** shape that runs a command taken from memory text. A
|
|
16
|
+
* memory can be written by the agent itself, so treating its content as an executable
|
|
17
|
+
* command is an injection path — the templates below only ever interpolate quoted file
|
|
18
|
+
* paths and literal search strings.
|
|
19
|
+
*
|
|
20
|
+
* @module context-memory/guard
|
|
21
|
+
*/
|
|
22
|
+
import type { Guard, GuardOutcome } from './types.js';
|
|
23
|
+
/**
|
|
24
|
+
* Compile a memory statement into an executable check.
|
|
25
|
+
*
|
|
26
|
+
* Commands are emitted for the platform in use. `process.platform` rather than a
|
|
27
|
+
* hardcoded POSIX spelling: `test -e` on Windows would leave every compiled memory
|
|
28
|
+
* permanently unrunnable, so verification would report on nothing while looking busy.
|
|
29
|
+
*/
|
|
30
|
+
export declare function compileGuard(text: string, platform?: NodeJS.Platform): Guard | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Quote a string so a POSIX shell treats it as one literal argument.
|
|
33
|
+
*
|
|
34
|
+
* Single quotes make every character literal; the only character that cannot appear
|
|
35
|
+
* inside them is the quote itself, escaped by closing, escaping, and reopening.
|
|
36
|
+
*/
|
|
37
|
+
export declare function shellQuote(value: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Run one guard to completion.
|
|
40
|
+
*
|
|
41
|
+
* `spawnSync` with a shell so compound commands work as written. No timeout: a guard
|
|
42
|
+
* that hangs is a hang, and killing it after N seconds would turn "slow" into an
|
|
43
|
+
* outcome that varies with machine load.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runGuard(id: string, guard: Guard, cwd: string, now?: Date): {
|
|
46
|
+
id: string;
|
|
47
|
+
outcome: GuardOutcome;
|
|
48
|
+
exitCode: null;
|
|
49
|
+
output: string;
|
|
50
|
+
at: string;
|
|
51
|
+
} | {
|
|
52
|
+
id: string;
|
|
53
|
+
outcome: GuardOutcome;
|
|
54
|
+
exitCode: number;
|
|
55
|
+
output: string;
|
|
56
|
+
at: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Classify an exit code into an outcome.
|
|
60
|
+
*
|
|
61
|
+
* Two conventions mean "the command does not exist": POSIX shells use 127, cmd.exe
|
|
62
|
+
* uses 9009. Both are recognised, along with the text shells print instead of a
|
|
63
|
+
* distinctive code when the failure happens inside the shell. Without this, a
|
|
64
|
+
* misspelled path would look like a memory being wrong about the world.
|
|
65
|
+
*/
|
|
66
|
+
export declare function classify(code: number, output?: string): GuardOutcome;
|
package/lib/guard.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard compilation and execution for memories.
|
|
3
|
+
*
|
|
4
|
+
* A memory is a claim, and this module is what decides whether the claim still holds.
|
|
5
|
+
* It keeps the same distinction dsh-verdict does, for the same reason: **a check that
|
|
6
|
+
* cannot run is not a memory that is wrong.** Conflating them would mean a mistyped
|
|
7
|
+
* path in a guard marks a perfectly good memory as false, and the user learns to
|
|
8
|
+
* ignore the output.
|
|
9
|
+
*
|
|
10
|
+
* Recognition is narrow on purpose. Only statement shapes whose checkable predicate is
|
|
11
|
+
* unambiguous get a guard; everything else stays uncheckable and says so. Emitting an
|
|
12
|
+
* approximate command would produce a check that exits 0 while verifying nothing, and
|
|
13
|
+
* every later "verified" would be a lie.
|
|
14
|
+
*
|
|
15
|
+
* There is deliberately **no** shape that runs a command taken from memory text. A
|
|
16
|
+
* memory can be written by the agent itself, so treating its content as an executable
|
|
17
|
+
* command is an injection path — the templates below only ever interpolate quoted file
|
|
18
|
+
* paths and literal search strings.
|
|
19
|
+
*
|
|
20
|
+
* @module context-memory/guard
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
/** Enough output to diagnose, small enough to keep inline in the event log. */
|
|
24
|
+
const OUTPUT_LIMIT = 2000;
|
|
25
|
+
/**
|
|
26
|
+
* Compile a memory statement into an executable check.
|
|
27
|
+
*
|
|
28
|
+
* Commands are emitted for the platform in use. `process.platform` rather than a
|
|
29
|
+
* hardcoded POSIX spelling: `test -e` on Windows would leave every compiled memory
|
|
30
|
+
* permanently unrunnable, so verification would report on nothing while looking busy.
|
|
31
|
+
*/
|
|
32
|
+
export function compileGuard(text, platform = process.platform) {
|
|
33
|
+
const normalized = text.trim().replace(/\s+/g, ' ');
|
|
34
|
+
const posix = platform !== 'win32';
|
|
35
|
+
const operand = (value) => (posix ? shellQuote(value) : `"${value.replace(/"/g, '\\"')}"`);
|
|
36
|
+
// "the file X must not contain Y" — checked before the positive form so that the
|
|
37
|
+
// negated sentence is never read as an assertion that Y is present.
|
|
38
|
+
const fileLacks = /^(?:the )?(?:file|path) `?([^\s`]+)`? (?:must not|should not|never) (?:contain|mention|include) `?([^\s`]+)`?$/i.exec(normalized);
|
|
39
|
+
if (fileLacks?.[1] && fileLacks[2]) {
|
|
40
|
+
const [file, needle] = [fileLacks[1], fileLacks[2]];
|
|
41
|
+
// A quote inside the operand cannot survive cmd.exe's own quoting layer; refusing
|
|
42
|
+
// beats emitting a command whose meaning differs from the sentence.
|
|
43
|
+
if (!posix && needle.includes('"'))
|
|
44
|
+
return undefined;
|
|
45
|
+
return {
|
|
46
|
+
command: posix
|
|
47
|
+
? `! grep -qF ${shellQuote(needle)} ${shellQuote(file)}`
|
|
48
|
+
: `findstr /c:${operand(needle)} ${operand(file)} >nul 2>nul && exit 1`,
|
|
49
|
+
source: 'template',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// "the file X contains Y" / "X mentions Y"
|
|
53
|
+
const fileHas = /^(?:the )?(?:file|path) `?([^\s`]+)`? (?:contains|mentions|includes) `?([^\s`]+)`?$/i.exec(normalized);
|
|
54
|
+
if (fileHas?.[1] && fileHas[2]) {
|
|
55
|
+
const [file, needle] = [fileHas[1], fileHas[2]];
|
|
56
|
+
if (!posix && needle.includes('"'))
|
|
57
|
+
return undefined;
|
|
58
|
+
return {
|
|
59
|
+
command: posix
|
|
60
|
+
? `grep -qF ${shellQuote(needle)} ${shellQuote(file)}`
|
|
61
|
+
: `findstr /c:${operand(needle)} ${operand(file)} >nul`,
|
|
62
|
+
source: 'template',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// "the file X exists" / "X exists" / "X is at <path>" is the same predicate.
|
|
66
|
+
const fileExists = /^(?:the )?(?:file|path) `?([^\s`]+)`? (?:exists|is present)$/i.exec(normalized)
|
|
67
|
+
?? /^`?([^\s`]+)`? exists$/i.exec(normalized);
|
|
68
|
+
if (fileExists?.[1]) {
|
|
69
|
+
const target = operand(fileExists[1]);
|
|
70
|
+
return {
|
|
71
|
+
command: posix ? `test -f ${target}` : `cmd /c if not exist ${target} exit 1`,
|
|
72
|
+
source: 'template',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// "there is no file X" / "never commit X"
|
|
76
|
+
const fileAbsent = /^(?:there is no|there are no|no) (?:file )?`?([^\s`]+)`?$/i.exec(normalized)
|
|
77
|
+
?? /^(?:never|do not|don't) (?:create|add|commit|write|touch) (?:the )?(?:file )?`?([^\s`]+)`?$/i.exec(normalized);
|
|
78
|
+
if (fileAbsent?.[1]) {
|
|
79
|
+
const target = operand(fileAbsent[1]);
|
|
80
|
+
return {
|
|
81
|
+
command: posix ? `! test -e ${target}` : `cmd /c if exist ${target} exit 1`,
|
|
82
|
+
source: 'template',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Quote a string so a POSIX shell treats it as one literal argument.
|
|
89
|
+
*
|
|
90
|
+
* Single quotes make every character literal; the only character that cannot appear
|
|
91
|
+
* inside them is the quote itself, escaped by closing, escaping, and reopening.
|
|
92
|
+
*/
|
|
93
|
+
export function shellQuote(value) {
|
|
94
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run one guard to completion.
|
|
98
|
+
*
|
|
99
|
+
* `spawnSync` with a shell so compound commands work as written. No timeout: a guard
|
|
100
|
+
* that hangs is a hang, and killing it after N seconds would turn "slow" into an
|
|
101
|
+
* outcome that varies with machine load.
|
|
102
|
+
*/
|
|
103
|
+
export function runGuard(id, guard, cwd, now = new Date()) {
|
|
104
|
+
const at = now.toISOString();
|
|
105
|
+
let proc;
|
|
106
|
+
try {
|
|
107
|
+
proc = spawnSync(guard.command, { cwd, shell: true, encoding: 'utf8' });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return { id, outcome: 'broken', exitCode: null, output: String(error), at };
|
|
111
|
+
}
|
|
112
|
+
const output = `${proc.stdout ?? ''}${proc.stderr ?? ''}`.slice(0, OUTPUT_LIMIT);
|
|
113
|
+
if (proc.error) {
|
|
114
|
+
return { id, outcome: 'broken', exitCode: null, output: `${output}${proc.error.message}`.slice(0, OUTPUT_LIMIT), at };
|
|
115
|
+
}
|
|
116
|
+
const code = proc.status;
|
|
117
|
+
if (code === null) {
|
|
118
|
+
return { id, outcome: 'broken', exitCode: null, output: output || 'terminated by signal', at };
|
|
119
|
+
}
|
|
120
|
+
return { id, outcome: classify(code, output), exitCode: code, output, at };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Classify an exit code into an outcome.
|
|
124
|
+
*
|
|
125
|
+
* Two conventions mean "the command does not exist": POSIX shells use 127, cmd.exe
|
|
126
|
+
* uses 9009. Both are recognised, along with the text shells print instead of a
|
|
127
|
+
* distinctive code when the failure happens inside the shell. Without this, a
|
|
128
|
+
* misspelled path would look like a memory being wrong about the world.
|
|
129
|
+
*/
|
|
130
|
+
export function classify(code, output = '') {
|
|
131
|
+
if (code === 0)
|
|
132
|
+
return 'passed';
|
|
133
|
+
if (code === 127 || code === 9009)
|
|
134
|
+
return 'broken';
|
|
135
|
+
if (/command not found|is not recognized|No such file or directory|cannot open/i.test(output)) {
|
|
136
|
+
return 'broken';
|
|
137
|
+
}
|
|
138
|
+
return 'violated';
|
|
139
|
+
}
|