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/lib/routes.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Host routes for the panel.
3
+ *
4
+ * Registered on the host's web connection when one exists. A host running headless
5
+ * simply skips registration and every other part of the plugin keeps working — the
6
+ * panel is a view of the log, not a precondition for keeping it.
7
+ *
8
+ * @module context-memory/routes
9
+ */
10
+ /** The panel path, shared with the browser half so the two cannot drift apart. */
11
+ export const MEMORY_PANEL_PATH = '/api/context-memory.panel';
12
+ export function registerMemoryRoutes(ctx, memory) {
13
+ // Injecting rather than reading `ctx.connection` directly: a plain property access on
14
+ // a service that was not declared throws, and the web connection is absent in headless
15
+ // hosts anyway.
16
+ ctx.inject(['connection'], (connectionCtx) => {
17
+ const connection = connectionCtx.connection;
18
+ connection.fetch.register({
19
+ path: MEMORY_PANEL_PATH,
20
+ methods: ['GET'],
21
+ requestBody: 'buffered',
22
+ // Rebuilt from the log on every request: the panel must show the file's state,
23
+ // not whatever was in memory when the host started.
24
+ fetch: () => Promise.resolve(Response.json(memory.panel(), { headers: { 'cache-control': 'no-store' } })),
25
+ });
26
+ connection.fetch.register({
27
+ path: '/api/context-memory.verify',
28
+ methods: ['POST'],
29
+ requestBody: 'buffered',
30
+ fetch: async () => {
31
+ const summary = memory.verify();
32
+ return Response.json(summary, {
33
+ // The verification is the answer, not an error: a violated memory is reported
34
+ // with 200 and the caller decides what it means. Only a broken guard is a
35
+ // defect in the request path itself, and that is reported in the payload.
36
+ headers: { 'cache-control': 'no-store' },
37
+ });
38
+ },
39
+ });
40
+ });
41
+ }
package/lib/store.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The append-only event log.
3
+ *
4
+ * Memory has to survive the process that wrote it, and the interesting question later
5
+ * is never "what is the current value" but "why does the agent believe this". An
6
+ * append-only log answers both: current state is a fold, and the fold is reproducible
7
+ * from the file alone. A mutable store would let the last writer decide what was
8
+ * always true.
9
+ *
10
+ * @module context-memory/store
11
+ */
12
+ import type { MemoryEvent } from './types.js';
13
+ export declare class MemoryStore {
14
+ private readonly projectDir;
15
+ private readonly dir;
16
+ private readonly logPath;
17
+ constructor(projectDir: string);
18
+ get path(): string;
19
+ append(event: MemoryEvent): void;
20
+ /**
21
+ * Read the whole log.
22
+ *
23
+ * A process killed mid-write leaves one torn trailing line. That is a benign, expected
24
+ * shape after a crash, and refusing to read the entire history over it would trade a
25
+ * recoverable loss for a total one — so a torn *final* line is dropped. Corruption
26
+ * anywhere else is a real problem and is reported rather than skipped, because
27
+ * silently ignoring a damaged record is how a log stops being evidence.
28
+ */
29
+ readAll(): MemoryEvent[];
30
+ }
package/lib/store.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The append-only event log.
3
+ *
4
+ * Memory has to survive the process that wrote it, and the interesting question later
5
+ * is never "what is the current value" but "why does the agent believe this". An
6
+ * append-only log answers both: current state is a fold, and the fold is reproducible
7
+ * from the file alone. A mutable store would let the last writer decide what was
8
+ * always true.
9
+ *
10
+ * @module context-memory/store
11
+ */
12
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ export class MemoryStore {
15
+ projectDir;
16
+ dir;
17
+ logPath;
18
+ constructor(projectDir) {
19
+ this.projectDir = projectDir;
20
+ this.dir = join(projectDir, '.memory');
21
+ this.logPath = join(this.dir, 'events.jsonl');
22
+ }
23
+ get path() {
24
+ return this.logPath;
25
+ }
26
+ append(event) {
27
+ if (!existsSync(this.dir))
28
+ mkdirSync(this.dir, { recursive: true });
29
+ // One `write` of one line: a crash can truncate the final line, but cannot interleave.
30
+ appendFileSync(this.logPath, `${JSON.stringify(event)}\n`, 'utf8');
31
+ }
32
+ /**
33
+ * Read the whole log.
34
+ *
35
+ * A process killed mid-write leaves one torn trailing line. That is a benign, expected
36
+ * shape after a crash, and refusing to read the entire history over it would trade a
37
+ * recoverable loss for a total one — so a torn *final* line is dropped. Corruption
38
+ * anywhere else is a real problem and is reported rather than skipped, because
39
+ * silently ignoring a damaged record is how a log stops being evidence.
40
+ */
41
+ readAll() {
42
+ if (!existsSync(this.logPath))
43
+ return [];
44
+ const lines = readFileSync(this.logPath, 'utf8').split('\n');
45
+ const events = [];
46
+ for (let index = 0; index < lines.length; index += 1) {
47
+ const line = lines[index].trim();
48
+ if (line.length === 0)
49
+ continue;
50
+ try {
51
+ events.push(JSON.parse(line));
52
+ }
53
+ catch (error) {
54
+ const isFinalLine = lines.slice(index + 1).every((rest) => rest.trim().length === 0);
55
+ if (isFinalLine)
56
+ break;
57
+ throw new Error(`${this.logPath}: line ${index + 1} is not valid JSON: ${String(error)}`);
58
+ }
59
+ }
60
+ return events;
61
+ }
62
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Types for dsh-context-memory.
3
+ *
4
+ * The split between `Memory` (what was recorded) and `MemoryState` (what is currently
5
+ * true of it) mirrors the store: `Memory` is the immutable payload of a `remember`
6
+ * event, `MemoryState` is that payload folded together with every later event about it.
7
+ * Nothing in this plugin mutates a recorded memory — a memory stops being active
8
+ * because a later event says so, never because a field was overwritten.
9
+ *
10
+ * @module context-memory/types
11
+ */
12
+ /** What kind of thing is being remembered. Affects presentation, not verification. */
13
+ export type MemoryKind = 'fact' | 'decision' | 'preference' | 'pitfall';
14
+ /** Who wrote the memory. Provenance, so "why does the agent think it knows this" has an answer. */
15
+ export type MemorySource = 'user' | 'agent' | 'plugin';
16
+ /** A memory is active until something explicitly supersedes it. */
17
+ export type MemoryStatus = 'active' | 'superseded';
18
+ /** The same three-outcome model dsh-verdict uses: inability to check is not failure. */
19
+ export type GuardOutcome = 'passed' | 'violated' | 'broken';
20
+ /** An executable check compiled from a memory statement. */
21
+ export interface Guard {
22
+ command: string;
23
+ source: 'template';
24
+ }
25
+ /** The immutable payload recorded by a `remember` event. */
26
+ export interface Memory {
27
+ id: string;
28
+ /** Normalised key a conflict is decided on; visible everywhere it is used. */
29
+ subject: string;
30
+ text: string;
31
+ kind: MemoryKind;
32
+ source: MemorySource;
33
+ createdAt: number;
34
+ sessionId?: string;
35
+ step?: number;
36
+ guard?: Guard;
37
+ }
38
+ /** A memory folded together with every later event about it. */
39
+ export interface MemoryState extends Memory {
40
+ status: MemoryStatus;
41
+ supersededReason?: string;
42
+ supersededBy?: string;
43
+ verification?: {
44
+ at: number;
45
+ outcome: GuardOutcome;
46
+ exitCode: number | null;
47
+ output: string;
48
+ };
49
+ }
50
+ /**
51
+ * The append-only record. Every question the panel answers is derived by folding these,
52
+ * so "what happened" is never decided by whoever last held the file.
53
+ */
54
+ export type MemoryEvent = {
55
+ type: 'remember';
56
+ at: number;
57
+ memory: Memory;
58
+ } | {
59
+ type: 'supersede';
60
+ at: number;
61
+ id: string;
62
+ reason: string;
63
+ replacedBy?: string;
64
+ } | {
65
+ type: 'verify';
66
+ at: number;
67
+ id: string;
68
+ outcome: GuardOutcome;
69
+ exitCode: number | null;
70
+ output: string;
71
+ };
72
+ /** One recall result, with the evidence for why it ranked where it did. */
73
+ export interface RecallHit {
74
+ memory: MemoryState;
75
+ score: number;
76
+ /** Query tokens this memory actually contains. */
77
+ matched: string[];
78
+ /** Human-readable breakdown of the score, so a ranking is never unexplainable. */
79
+ why: string;
80
+ }
81
+ /** Two or more active memories sharing a subject but disagreeing about it. */
82
+ export interface Conflict {
83
+ subject: string;
84
+ memories: MemoryState[];
85
+ }
86
+ /** One verification result per memory that was considered. */
87
+ export interface VerifyResult {
88
+ id: string;
89
+ text: string;
90
+ /** `uncheckable` means no guard could be compiled — not that the memory is wrong. */
91
+ outcome: GuardOutcome | 'uncheckable';
92
+ exitCode: number | null;
93
+ output: string;
94
+ }
95
+ export interface VerifySummary {
96
+ passed: number;
97
+ violated: number;
98
+ broken: number;
99
+ uncheckable: number;
100
+ results: VerifyResult[];
101
+ }
102
+ /** Everything the panel needs, recomputed from the log on each read. */
103
+ export interface PanelPayload {
104
+ active: MemoryState[];
105
+ superseded: MemoryState[];
106
+ conflicts: Conflict[];
107
+ totals: {
108
+ active: number;
109
+ superseded: number;
110
+ withGuard: number;
111
+ verified: number;
112
+ unverified: number;
113
+ };
114
+ }
package/lib/types.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Types for dsh-context-memory.
3
+ *
4
+ * The split between `Memory` (what was recorded) and `MemoryState` (what is currently
5
+ * true of it) mirrors the store: `Memory` is the immutable payload of a `remember`
6
+ * event, `MemoryState` is that payload folded together with every later event about it.
7
+ * Nothing in this plugin mutates a recorded memory — a memory stops being active
8
+ * because a later event says so, never because a field was overwritten.
9
+ *
10
+ * @module context-memory/types
11
+ */
12
+ export {};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "dsh-context-memory",
3
+ "version": "0.1.0",
4
+ "description": "Cross-session memory for your dsh agent: remembered, recalled, and re-verified instead of assumed.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "bin": { "dsh-context-memory": "lib/bin.js" },
9
+ "exports": {
10
+ ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
11
+ "./client": { "types": "./lib/client.d.ts", "default": "./lib/memory.web.js" },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": ["lib", "cordis.patch.yml", "README.md", "CHANGELOG.md"],
15
+ "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "memory", "context", "recall", "persistence"],
16
+ "dsh": {
17
+ "bundle": { "patch": "./cordis.patch.yml" },
18
+ "client": {
19
+ "platform": "web",
20
+ "inject": ["@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-connection"]
21
+ }
22
+ },
23
+ "license": "MIT",
24
+ "author": "hj01857655",
25
+ "repository": { "type": "git", "url": "git+https://github.com/hj01857655/dsh-context-memory.git" },
26
+ "bugs": { "url": "https://github.com/hj01857655/dsh-context-memory/issues" },
27
+ "homepage": "https://github.com/hj01857655/dsh-context-memory#readme",
28
+ "engines": { "node": ">=20" },
29
+ "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1" },
30
+ "devDependencies": {
31
+ "@deepseek-ai/cordis": "^4.0.2",
32
+ "@types/node": "^22.20.3",
33
+ "@types/react": "^19.3.0",
34
+ "esbuild": "^0.28.2",
35
+ "react": "^19.3.0",
36
+ "react-dom": "^19.3.0",
37
+ "typescript": "^5.6.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json && node scripts/bundle-client.mjs",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json",
42
+ "test": "tsc -p tsconfig.json && node --test",
43
+ "prepublishOnly": "npm run build && npm test"
44
+ }
45
+ }