dsh-opencode-go-router 0.0.0-stage → 0.1.6

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/store.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Persistent route + performance state for opencode-router.
3
+ *
4
+ * The file contains only session ids, generated routing ids, fold numbers and
5
+ * timing summaries. No prompts, messages or tool output are stored.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import fsp from 'node:fs/promises';
10
+ import path from 'node:path';
11
+
12
+ const STATE_VERSION = 1;
13
+
14
+ function emptyState() {
15
+ return { version: STATE_VERSION, updatedAt: 0, routes: {}, metrics: {} };
16
+ }
17
+
18
+ function finiteOrNull(value) {
19
+ if (value == null || value === '') return null;
20
+ const n = Number(value);
21
+ return Number.isFinite(n) ? n : null;
22
+ }
23
+
24
+ function sanitizeRoute(entry) {
25
+ if (!entry || typeof entry !== 'object') return null;
26
+ const value = typeof entry.value === 'string' ? entry.value.slice(0, 256) : '';
27
+ if (value === '') return null;
28
+ return {
29
+ value,
30
+ rotatedAt: finiteOrNull(entry.rotatedAt),
31
+ foldSeq: finiteOrNull(entry.foldSeq),
32
+ reason: typeof entry.reason === 'string' ? entry.reason.slice(0, 200) : 'unknown',
33
+ cacheHitPct: finiteOrNull(entry.cacheHitPct),
34
+ avgTtftMs: finiteOrNull(entry.avgTtftMs),
35
+ avgTps: finiteOrNull(entry.avgTps),
36
+ };
37
+ }
38
+
39
+ function sanitizeMetric(entry) {
40
+ if (!entry || typeof entry !== 'object') return null;
41
+ const ttftMs = finiteOrNull(entry.ttftMs);
42
+ const tps = finiteOrNull(entry.tps);
43
+ const outputTokens = finiteOrNull(entry.outputTokens);
44
+ const at = finiteOrNull(entry.at);
45
+ if (ttftMs == null || ttftMs < 0 || tps == null || tps <= 0) return null;
46
+ return {
47
+ ttftMs,
48
+ tps,
49
+ outputTokens: outputTokens == null ? 0 : outputTokens,
50
+ at: at == null ? 0 : at,
51
+ };
52
+ }
53
+
54
+ function sanitizeState(raw) {
55
+ const state = emptyState();
56
+ if (!raw || typeof raw !== 'object') return state;
57
+ state.updatedAt = finiteOrNull(raw.updatedAt) || 0;
58
+ if (raw.routes && typeof raw.routes === 'object') {
59
+ for (const [sessionId, entry] of Object.entries(raw.routes)) {
60
+ const route = sanitizeRoute(entry);
61
+ if (route) state.routes[String(sessionId)] = route;
62
+ }
63
+ }
64
+ if (raw.metrics && typeof raw.metrics === 'object') {
65
+ for (const [sessionId, list] of Object.entries(raw.metrics)) {
66
+ if (!Array.isArray(list)) continue;
67
+ const samples = list.map(sanitizeMetric).filter(Boolean).slice(-100);
68
+ if (samples.length > 0) state.metrics[String(sessionId)] = samples;
69
+ }
70
+ }
71
+ return state;
72
+ }
73
+
74
+ export class RouterStore {
75
+ constructor(options) {
76
+ const opts = options || {};
77
+ this.statePath = opts.statePath;
78
+ this.logger = opts.logger || console;
79
+ this.now = typeof opts.now === 'function' ? opts.now : Date.now;
80
+ this.debounceMs = Number.isFinite(Number(opts.debounceMs)) ? Number(opts.debounceMs) : 150;
81
+ this.state = this.load();
82
+ this.dirty = false;
83
+ this.timer = null;
84
+ this.chain = Promise.resolve();
85
+ }
86
+
87
+ load() {
88
+ try {
89
+ const text = fs.readFileSync(this.statePath, 'utf8');
90
+ return sanitizeState(JSON.parse(text));
91
+ } catch (error) {
92
+ if (error && error.code !== 'ENOENT') {
93
+ this.warn('state file unreadable; starting empty: ' + String(error && error.message ? error.message : error));
94
+ try {
95
+ if (fs.existsSync(this.statePath)) {
96
+ const backup = this.statePath + '.corrupt-' + Date.now();
97
+ fs.copyFileSync(this.statePath, backup);
98
+ this.warn('corrupt state backed up to ' + backup);
99
+ }
100
+ } catch {
101
+ // Best effort only.
102
+ }
103
+ }
104
+ return emptyState();
105
+ }
106
+ }
107
+
108
+ warn(line) {
109
+ try {
110
+ const logger = this.logger;
111
+ if (logger && typeof logger.warn === 'function') logger.warn('[opencode-router] ' + line);
112
+ else if (typeof console !== 'undefined' && console.warn) console.warn('[opencode-router] ' + line);
113
+ } catch {
114
+ // Logging must never break routing.
115
+ }
116
+ }
117
+
118
+ routeFor(sessionId) {
119
+ const key = String(sessionId == null ? '' : sessionId);
120
+ if (key === '') return null;
121
+ const route = this.state.routes[key];
122
+ return route ? { ...route } : null;
123
+ }
124
+
125
+ setRoute(sessionId, record) {
126
+ const key = String(sessionId == null ? '' : sessionId);
127
+ if (key === '') return null;
128
+ const clean = sanitizeRoute(record);
129
+ if (!clean) return null;
130
+ this.state.routes[key] = clean;
131
+ this.touch();
132
+ return clean;
133
+ }
134
+
135
+ recordSample(sessionId, sample, maxSamples) {
136
+ const key = String(sessionId == null ? '' : sessionId);
137
+ const clean = sanitizeMetric(sample);
138
+ if (key === '' || !clean) return;
139
+ const limit = Math.max(1, Math.round(Number(maxSamples) || 20));
140
+ const list = Array.isArray(this.state.metrics[key]) ? this.state.metrics[key] : [];
141
+ list.push(clean);
142
+ this.state.metrics[key] = list.slice(-limit);
143
+ this.touch();
144
+ }
145
+
146
+ samplesFor(sessionId) {
147
+ const key = String(sessionId == null ? '' : sessionId);
148
+ const list = key === '' ? null : this.state.metrics[key];
149
+ return Array.isArray(list) ? list.map((sample) => ({ ...sample })) : [];
150
+ }
151
+
152
+ touch() {
153
+ this.state.updatedAt = this.now();
154
+ this.dirty = true;
155
+ this.schedulePersist();
156
+ }
157
+
158
+ schedulePersist() {
159
+ if (this.timer) clearTimeout(this.timer);
160
+ this.timer = setTimeout(() => {
161
+ this.timer = null;
162
+ this.flush().catch(() => {});
163
+ }, this.debounceMs);
164
+ if (this.timer && typeof this.timer.unref === 'function') this.timer.unref();
165
+ }
166
+
167
+ async flush() {
168
+ if (this.timer) {
169
+ clearTimeout(this.timer);
170
+ this.timer = null;
171
+ }
172
+ if (!this.dirty) return this.chain;
173
+ this.dirty = false;
174
+ const payload = JSON.stringify(this.state, null, 2);
175
+ const target = this.statePath;
176
+ const dir = path.dirname(target);
177
+ this.chain = this.chain
178
+ .then(async () => {
179
+ await fsp.mkdir(dir, { recursive: true });
180
+ const tmp = target + '.tmp-' + process.pid + '-' + Date.now();
181
+ await fsp.writeFile(tmp, payload, 'utf8');
182
+ await fsp.rename(tmp, target);
183
+ })
184
+ .catch((error) => {
185
+ this.dirty = true;
186
+ this.warn('state write failed: ' + String(error && error.message ? error.message : error));
187
+ });
188
+ return this.chain;
189
+ }
190
+
191
+ snapshot() {
192
+ return JSON.parse(JSON.stringify(this.state));
193
+ }
194
+
195
+ async dispose() {
196
+ try {
197
+ await this.flush();
198
+ } catch {
199
+ // Best effort.
200
+ }
201
+ }
202
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "dsh-opencode-go-router",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.6",
4
+ "description": "DeepSeek Harness plugin: rotate OpenCode Go routing after billion-context cache-losing folds or system-prompt rewrites.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "lib",
13
+ "cordis.patch.yml",
14
+ "README.md",
15
+ "README.zh-CN.md",
16
+ "CHANGELOG.md",
17
+ "CHANGELOG.en.md",
18
+ "LICENSE"
19
+ ],
20
+ "dsh": {
21
+ "manifestVersion": 1,
22
+ "engines": {
23
+ "dsh": ">=0.2.1-rc.5 <0.3.0-0"
24
+ },
25
+ "bundle": {
26
+ "patch": "./cordis.patch.yml"
27
+ }
28
+ },
29
+ "keywords": [
30
+ "deepseek",
31
+ "deepseek-harness",
32
+ "dsh",
33
+ "dsh-plugin",
34
+ "opencode-go",
35
+ "billion-context",
36
+ "prompt-cache",
37
+ "cache",
38
+ "routing",
39
+ "session"
40
+ ],
41
+ "author": "TLEWA",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/TLEWA/dsh-opencode-go-router.git"
46
+ },
47
+ "homepage": "https://github.com/TLEWA/dsh-opencode-go-router#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/TLEWA/dsh-opencode-go-router/issues"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "engines": {
56
+ "node": ">=20"
57
+ },
58
+ "scripts": {
59
+ "check": "node --check lib/index.js && node --check lib/core.js && node --check lib/impl.js && node --check lib/monitor.js && node --check lib/store.js && node --check test/core.test.js && node --check test/impl.test.js && node --check test/monitor.test.js && node --check test/store.test.js",
60
+ "test": "node --test",
61
+ "prepack": "npm run check && npm test",
62
+ "prepublishOnly": "npm run check && npm test"
63
+ }
64
+ }