opencode-plugboard 0.21.1 → 0.21.2

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 +2 -2
  2. package/package.json +1 -1
  3. package/src/usage.ts +24 -23
package/README.md CHANGED
@@ -43,7 +43,7 @@ export PLUGBOARD_KEY_MY_APP=choose-a-long-random-string
43
43
  {
44
44
  "plugins": [
45
45
  {
46
- "package": "opencode-plugboard@0.21.1",
46
+ "package": "opencode-plugboard@0.21.2",
47
47
  "options": {
48
48
  "keys": { "my-app": "{env:PLUGBOARD_KEY_MY_APP}" },
49
49
  "models": ["anthropic/claude-sonnet-4-6", "github-copilot/*", "opencode-go/glm-5.3-flash"],
@@ -390,7 +390,7 @@ A server shared by a Discord bot, n8n, Open WebUI and a coding agent, with most
390
390
  {
391
391
  "plugins": [
392
392
  {
393
- "package": "opencode-plugboard@0.21.1",
393
+ "package": "opencode-plugboard@0.21.2",
394
394
  "options": {
395
395
  // Reachable from the LAN. Non-loopback hosts refuse to start without keys.
396
396
  "listen": { "host": "10.0.1.50", "port": 4010 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-plugboard",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Use the ChatGPT, Claude, Copilot and OpenCode Go logins in your OpenCode server from any app: an OpenCode V2 plugin serving OpenAI, Anthropic and Gemini compatible APIs.",
package/src/usage.ts CHANGED
@@ -9,10 +9,11 @@
9
9
  * Where it lives: the live aggregate is in memory on the process runtime (`src/runtime.ts`), so it
10
10
  * survives a plugin generation, and each day is persisted to the plugin's host storage as JSON under
11
11
  * `usage/<app>/<YYYY-MM-DD>`. Every write comes from request handling (`withUsage` and the route's
12
- * request log point), never from plugin setup or a finalizer (docs/architecture.md, I23). A day is
13
- * written at most every `FLUSH_MS`: a change is persisted by the first request that finds the last
14
- * write that old, so the stored copy can lag a quiet period behind memory. Queries read memory first,
15
- * so a report is never stale.
12
+ * request log point), never from plugin setup or a finalizer (docs/architecture.md, I23). A change is
13
+ * written as it lands; while a write of that day is in flight, later changes wait for it and go out
14
+ * in the next write, so the stored copy keeps up with every change (#241: a debounce kept the request
15
+ * count of a burst in memory, and a restart dropped it). Queries read memory first, so a
16
+ * report is never stale.
16
17
  *
17
18
  * Retention: days older than `usage.retentionDays` are deleted from request handling, one bounded
18
19
  * page of keys at a time and at most once per `PRUNE_EVERY_MS`.
@@ -48,8 +49,6 @@ export const GENERATION_ROUTES: ReadonlySet<string> = new Set([
48
49
 
49
50
  const DAY_MS = 86_400_000
50
51
  const KEY_PREFIX = "usage/"
51
- /** A changed day is written at most this often. */
52
- const FLUSH_MS = 10_000
53
52
  /** Retention runs at most this often, and each run reads one page of keys. */
54
53
  const PRUNE_EVERY_MS = 10 * 60_000
55
54
  const PRUNE_PAGE = 200
@@ -417,7 +416,6 @@ interface Entry {
417
416
  /** Bumped by every change; `flushedVersion` is the version last persisted. */
418
417
  version: number
419
418
  flushedVersion: number
420
- flushedAt: number
421
419
  flushing?: Promise<void>
422
420
  }
423
421
 
@@ -517,28 +515,31 @@ export function createUsageRecorder(deps: UsageRecorderDeps): UsageRecorder {
517
515
  // Another request may have loaded the day while this one read storage: keep the first entry.
518
516
  const current = state.entries.get(key)
519
517
  if (current) return current
520
- const entry: Entry = { key, app, date, day: stored ?? emptyDay(), version: 0, flushedVersion: 0, flushedAt: 0 }
518
+ const entry: Entry = { key, app, date, day: stored ?? emptyDay(), version: 0, flushedVersion: 0 }
521
519
  state.entries.set(key, entry)
522
520
  return entry
523
521
  }
524
522
 
525
- /** Persists a changed day when the last write is older than `FLUSH_MS`. The JSON is taken now; a later change waits for the next write. */
526
- function flushIfDue(entry: Entry, at: number): void {
527
- if (entry.version === entry.flushedVersion || entry.flushing !== undefined || at - entry.flushedAt < FLUSH_MS) return
523
+ /**
524
+ * Persists a changed day. The JSON is taken now. A write already in flight is not raced: when it lands,
525
+ * a change made meanwhile is written next. A failed write is logged and the day stays changed, so the
526
+ * next change tries again.
527
+ */
528
+ function flushIfDue(entry: Entry): void {
529
+ if (entry.version === entry.flushedVersion || entry.flushing !== undefined) return
528
530
  const version = entry.version
529
531
  const json = JSON.stringify(entry.day)
530
- entry.flushedAt = at
531
- entry.flushing = storage
532
- .put(entry.key, json)
533
- .then(
534
- () => {
535
- entry.flushedVersion = version
536
- },
537
- (error) => logError("usage.flush_failed", { app: entry.app, date: entry.date, error: errorMessage(error) }),
538
- )
539
- .finally(() => {
532
+ entry.flushing = storage.put(entry.key, json).then(
533
+ () => {
534
+ entry.flushedVersion = version
540
535
  entry.flushing = undefined
541
- })
536
+ flushIfDue(entry)
537
+ },
538
+ (error) => {
539
+ entry.flushing = undefined
540
+ logError("usage.flush_failed", { app: entry.app, date: entry.date, error: errorMessage(error) })
541
+ },
542
+ )
542
543
  }
543
544
 
544
545
  /** Deletes the days older than the retention window: one page of keys per run, at most once per `PRUNE_EVERY_MS`. */
@@ -576,7 +577,7 @@ export function createUsageRecorder(deps: UsageRecorderDeps): UsageRecorder {
576
577
  if (!entry) return
577
578
  update(entry.day)
578
579
  entry.version++
579
- flushIfDue(entry, at)
580
+ flushIfDue(entry)
580
581
  maybePrune(at)
581
582
  })
582
583
  .catch((error) => logError("usage.record_failed", { app, error: errorMessage(error) }))