@witness-ai/opencode 0.6.1 → 0.7.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/README.md +6 -0
- package/package.json +3 -3
- package/witness.js +36 -12
package/README.md
CHANGED
|
@@ -52,6 +52,12 @@ npm exec --yes --package=@witness-ai/opencode@beta -- witness doctor
|
|
|
52
52
|
npm exec --yes --package=@witness-ai/opencode@beta -- witness distill status
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
> **Upgrade note:** Older witness releases could leave an OpenCode session whose agent or title is
|
|
56
|
+
> `witness-distill` when distillation was interrupted. Current releases no longer create or filter
|
|
57
|
+
> those sessions in OpenCode's user database. Before the first import after upgrading, remove any
|
|
58
|
+
> leftover `witness-distill` sessions with OpenCode's supported session-management tools so they are
|
|
59
|
+
> not captured as normal archive data.
|
|
60
|
+
|
|
55
61
|
By default the model is downloaded from Hugging Face. A custom `WITNESS_MODEL_BASE_URL` must serve the same paths (`onnx/model.onnx` and `tokenizer.json`) and also set `WITNESS_MODEL_SHA256` plus `WITNESS_TOKENIZER_SHA256`.
|
|
56
62
|
|
|
57
63
|
Automatic distillation is batched by default: sessions are reconciled on startup and idle, while model work starts at most once every 10 minutes, drains the current queue, then exits so the embed model is not resident. Edit witness `config.toml` if you want manual-only behavior:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witness-ai/opencode",
|
|
3
3
|
"mcpName": "io.github.IngTian/witness",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "MCP memory / second brain for Claude Code & OpenCode — captures your coding sessions and distills how your knowledge and habits evolve over time, queryable by your agent.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./witness.js",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@witness-ai/opencode-darwin-arm64": "0.
|
|
25
|
-
"@witness-ai/opencode-linux-x64": "0.
|
|
24
|
+
"@witness-ai/opencode-darwin-arm64": "0.7.0",
|
|
25
|
+
"@witness-ai/opencode-linux-x64": "0.7.0"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
28
28
|
"mcp",
|
package/witness.js
CHANGED
|
@@ -8,6 +8,7 @@ const DOWNLOAD_RETRY_MAX = 6
|
|
|
8
8
|
const DOWNLOAD_RETRY_BASE_MS = 1000
|
|
9
9
|
const DOWNLOAD_RETRY_CAP_MS = 30000
|
|
10
10
|
const IMPORT_GRACE_MS = 5000
|
|
11
|
+
const QUIET_PERIOD_MS = 5 * 60 * 1000
|
|
11
12
|
|
|
12
13
|
const platformBin = platformWitnessBin()
|
|
13
14
|
const WITNESS_BIN = globalThis.WITNESS_SHIM || process.env.WITNESS_BIN || (existsSync(platformBin) ? platformBin : "")
|
|
@@ -78,13 +79,14 @@ const plugin = async () => {
|
|
|
78
79
|
let disposing = false
|
|
79
80
|
let disposePromise = null
|
|
80
81
|
let retryTimer = null
|
|
82
|
+
let quietTimer = null
|
|
81
83
|
let download = null
|
|
82
84
|
let activeImport = null
|
|
83
85
|
let activeWaiters = null
|
|
84
86
|
let globalImportPending = false
|
|
85
87
|
const pendingSessions = new Set()
|
|
86
88
|
const sessionWaiters = new Map()
|
|
87
|
-
const
|
|
89
|
+
const idleCycles = new Map()
|
|
88
90
|
const idleWaiters = []
|
|
89
91
|
|
|
90
92
|
function clearRetry() {
|
|
@@ -92,6 +94,25 @@ const plugin = async () => {
|
|
|
92
94
|
retryTimer = null
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
function clearQuietTimer() {
|
|
98
|
+
if (quietTimer) clearTimeout(quietTimer)
|
|
99
|
+
quietTimer = null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scheduleQuietWorker() {
|
|
103
|
+
clearQuietTimer()
|
|
104
|
+
quietTimer = setTimeout(() => {
|
|
105
|
+
quietTimer = null
|
|
106
|
+
if (disposed || disposing) return
|
|
107
|
+
// worker-kick, NOT `worker-run --auto`: dispose below runs `worker stop
|
|
108
|
+
// --auto-only`, which latches a durable worker_stop_requested flag that an auto
|
|
109
|
+
// worker refuses to run under. Only the shared kick gate clears it, so spawning
|
|
110
|
+
// worker-run directly would no-op forever after the first OpenCode close.
|
|
111
|
+
spawnWitness(["worker-kick"])
|
|
112
|
+
}, QUIET_PERIOD_MS)
|
|
113
|
+
quietTimer.unref?.()
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
function scheduleRetry(attempt) {
|
|
96
117
|
if (disposed || disposing || process.env.WITNESS_SKIP_MODEL_DOWNLOAD === "1" || modelReady(PACKAGE_ROOT) || attempt > DOWNLOAD_RETRY_MAX) return
|
|
97
118
|
clearRetry()
|
|
@@ -123,7 +144,7 @@ const plugin = async () => {
|
|
|
123
144
|
}
|
|
124
145
|
|
|
125
146
|
function syncOpenCode(sessions = []) {
|
|
126
|
-
const args = ["import", "--agent", "opencode", "--quiet", "--
|
|
147
|
+
const args = ["import", "--agent", "opencode", "--quiet", "--no-kick"]
|
|
127
148
|
for (const sessionID of sessions) args.push("--session", sessionID)
|
|
128
149
|
const proc = spawnWitness(args)
|
|
129
150
|
if (!proc) return
|
|
@@ -225,7 +246,8 @@ const plugin = async () => {
|
|
|
225
246
|
if (disposePromise) return disposePromise
|
|
226
247
|
disposing = true
|
|
227
248
|
clearRetry()
|
|
228
|
-
|
|
249
|
+
clearQuietTimer()
|
|
250
|
+
idleCycles.clear()
|
|
229
251
|
disposePromise = (async () => {
|
|
230
252
|
let timer
|
|
231
253
|
const drained = await Promise.race([
|
|
@@ -251,8 +273,8 @@ const plugin = async () => {
|
|
|
251
273
|
await waitForExit(download?.child)
|
|
252
274
|
download = null
|
|
253
275
|
// Only stop automatically-started workers. A user may have explicitly run
|
|
254
|
-
// `witness
|
|
255
|
-
const proc = spawnWitness(["
|
|
276
|
+
// `witness worker run --detach`; closing OpenCode must not kill that manual job.
|
|
277
|
+
const proc = spawnWitness(["worker", "stop", "--auto-only"])
|
|
256
278
|
await proc?.exited?.catch?.(() => {})
|
|
257
279
|
})()
|
|
258
280
|
return disposePromise
|
|
@@ -262,19 +284,21 @@ const plugin = async () => {
|
|
|
262
284
|
const type = eventType(event)
|
|
263
285
|
const sessionID = event?.properties?.sessionID
|
|
264
286
|
const modernIdle = type === "session.status" && event?.properties?.status?.type === "idle"
|
|
265
|
-
if (type === "session.idle" && sessionID && modernIdleWaiters.has(sessionID)) {
|
|
266
|
-
const done = modernIdleWaiters.get(sessionID)
|
|
267
|
-
modernIdleWaiters.delete(sessionID)
|
|
268
|
-
return done
|
|
269
|
-
}
|
|
270
287
|
if ((type === "session.idle" || modernIdle) && sessionID) {
|
|
288
|
+
const existing = idleCycles.get(sessionID)
|
|
289
|
+
if (existing) return existing
|
|
271
290
|
clearRetry()
|
|
272
291
|
ensureDownload()
|
|
273
292
|
const done = syncSessions(sessionID)
|
|
274
|
-
|
|
293
|
+
idleCycles.set(sessionID, done)
|
|
294
|
+
scheduleQuietWorker()
|
|
275
295
|
return done
|
|
276
296
|
}
|
|
277
|
-
|
|
297
|
+
const sessionActivity = sessionID && (type.startsWith("message.") || type.startsWith("session."))
|
|
298
|
+
if (sessionActivity) {
|
|
299
|
+
if (sessionID) idleCycles.delete(sessionID)
|
|
300
|
+
clearQuietTimer()
|
|
301
|
+
}
|
|
278
302
|
},
|
|
279
303
|
}
|
|
280
304
|
}
|