@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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/package.json +3 -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.6.1",
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.6.1",
25
- "@witness-ai/opencode-linux-x64": "0.6.1"
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 modernIdleWaiters = new Map()
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", "--auto"]
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
- modernIdleWaiters.clear()
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 distill start`; closing OpenCode must not kill that manual job.
255
- const proc = spawnWitness(["distill", "stop", "--auto-only"])
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
- if (modernIdle) modernIdleWaiters.set(sessionID, done)
293
+ idleCycles.set(sessionID, done)
294
+ scheduleQuietWorker()
275
295
  return done
276
296
  }
277
- if (type === "session.status" && sessionID) modernIdleWaiters.delete(sessionID)
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
  }