context-mode 1.0.129 → 1.0.131
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/db-base.d.ts +20 -0
- package/build/db-base.js +35 -79
- package/build/session/analytics.js +36 -10
- package/cli.bundle.mjs +126 -126
- package/hooks/session-db.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +111 -111
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.131"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.131",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.131",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.131",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.131",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/build/db-base.d.ts
CHANGED
|
@@ -139,6 +139,26 @@ export declare function isSQLiteCorruptionError(msg: string): boolean;
|
|
|
139
139
|
export declare function renameCorruptDB(dbPath: string): void;
|
|
140
140
|
export declare abstract class SQLiteBase {
|
|
141
141
|
#private;
|
|
142
|
+
/**
|
|
143
|
+
* Open (or create) a SQLite DB at `dbPath`.
|
|
144
|
+
*
|
|
145
|
+
* v1.0.130 — multi-writer is the contract. ALL SQLiteBase consumers
|
|
146
|
+
* (SessionDB, ContentStore) may open the same on-disk dbPath from
|
|
147
|
+
* multiple processes simultaneously — that is the legitimate multi-
|
|
148
|
+
* window UX shape and the WAL handles it natively. SQLITE_BUSY on
|
|
149
|
+
* write contention is absorbed by `withRetry()` below (busy_timeout
|
|
150
|
+
* = 30000ms inside `new Database(...)`).
|
|
151
|
+
*
|
|
152
|
+
* v1.0.128 introduced a single-writer guard here as a defense against
|
|
153
|
+
* #560. That defense was an over-correction — the actual root causes
|
|
154
|
+
* of #560 were #559 (zombie MCP child accumulation) and #561 (Pi
|
|
155
|
+
* misdetection writing to the wrong DB path), both fixed in v1.0.128
|
|
156
|
+
* + v1.0.129. The single-writer guard broke legitimate multi-window
|
|
157
|
+
* users; v1.0.130 rolls it out. See
|
|
158
|
+
* docs/adr/0001-sessiondb-multi-writer.md and the v1.0.130 INVARIANT
|
|
159
|
+
* block in tests/util/db-base-platform-gate.test.ts for the
|
|
160
|
+
* regression-proof anchor (source-pin + behavioural).
|
|
161
|
+
*/
|
|
142
162
|
constructor(dbPath: string);
|
|
143
163
|
/** Called once after WAL pragmas are applied. Subclasses run CREATE TABLE/VIRTUAL TABLE here. */
|
|
144
164
|
protected abstract initSchema(): void;
|
package/build/db-base.js
CHANGED
|
@@ -9,12 +9,6 @@ import { createRequire } from "node:module";
|
|
|
9
9
|
import { existsSync, unlinkSync, renameSync } from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
// v1.0.128 — Issue #560 single-writer enforcement.
|
|
13
|
-
// Lockfile is the PRIMARY defense (clean UX with conflicting PID),
|
|
14
|
-
// `locking_mode = EXCLUSIVE` (applied in applyWALPragmas below) is the
|
|
15
|
-
// SECONDARY defense for the narrow race window between lockfile claim
|
|
16
|
-
// and the actual `new Database(...)` open. Both skip-gate on tmpdir paths.
|
|
17
|
-
import { acquireDbLock, releaseDbLock } from "./util/db-lock.js";
|
|
18
12
|
// ─────────────────────────────────────────────────────────
|
|
19
13
|
// bun:sqlite adapter (#45)
|
|
20
14
|
// ─────────────────────────────────────────────────────────
|
|
@@ -317,14 +311,12 @@ export function applyWALPragmas(db) {
|
|
|
317
311
|
}
|
|
318
312
|
catch { /* unsupported runtime */ }
|
|
319
313
|
// NOTE: `locking_mode = EXCLUSIVE` is intentionally NOT applied here.
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
// Different DB paths from different worktrees/sessions ARE concurrent
|
|
327
|
-
// by design — the lockfile is per-dbPath, not per-process.
|
|
314
|
+
// ALL DBs built on this helper — ContentStore (FTS5 shared knowledge
|
|
315
|
+
// base) AND SessionDB (per-project events) — are multi-writer-safe by
|
|
316
|
+
// contract. WAL + busy_timeout + the withRetry() wrapper below handle
|
|
317
|
+
// SQLITE_BUSY natively. EXCLUSIVE locking is opt-out, never opt-in
|
|
318
|
+
// from a base class shared by multi-writer consumers.
|
|
319
|
+
// See docs/adr/0001-sessiondb-multi-writer.md for the v1.0.130 ADR.
|
|
328
320
|
}
|
|
329
321
|
// ─────────────────────────────────────────────────────────
|
|
330
322
|
// DB file helpers
|
|
@@ -465,26 +457,19 @@ export function renameCorruptDB(dbPath) {
|
|
|
465
457
|
* re-imports within the same fork process (ESM isolate mode clears
|
|
466
458
|
* module-level state but globalThis persists).
|
|
467
459
|
*/
|
|
468
|
-
// v1.0.
|
|
469
|
-
//
|
|
470
|
-
// A persistent global slot from a
|
|
471
|
-
// the wrong shape and crash the
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// owns its lockfile. The exit hook needs both — closeDB(db) handles the
|
|
475
|
-
// WAL checkpoint, releaseDbLock(dbPath) drops the .lock file. We use a
|
|
476
|
-
// Map keyed by DatabaseInstance to keep API call sites unchanged.
|
|
460
|
+
// v1.0.130 — symbol name bumped because the value type reverted from
|
|
461
|
+
// Map<DatabaseInstance, string> (v1.0.128 lockfile pairing) back to
|
|
462
|
+
// Set<DatabaseInstance>. A persistent global slot from a v1.0.128 or
|
|
463
|
+
// v1.0.129 module would deserialize as the wrong shape and crash the
|
|
464
|
+
// exit hook iteration.
|
|
465
|
+
const _kLiveDBs = Symbol.for("__context_mode_live_dbs_v3__");
|
|
477
466
|
const _liveDBs = (() => {
|
|
478
467
|
const g = globalThis;
|
|
479
468
|
if (!g[_kLiveDBs]) {
|
|
480
|
-
g[_kLiveDBs] = new
|
|
469
|
+
g[_kLiveDBs] = new Set();
|
|
481
470
|
process.on("exit", () => {
|
|
482
|
-
for (const
|
|
471
|
+
for (const db of g[_kLiveDBs]) {
|
|
483
472
|
closeDB(db);
|
|
484
|
-
// Release lock AFTER close so the WAL checkpoint inside closeDB
|
|
485
|
-
// runs while we still own the writer slot (no second-opener can
|
|
486
|
-
// race in mid-checkpoint).
|
|
487
|
-
releaseDbLock({ dbPath });
|
|
488
473
|
}
|
|
489
474
|
g[_kLiveDBs].clear();
|
|
490
475
|
});
|
|
@@ -494,45 +479,34 @@ const _liveDBs = (() => {
|
|
|
494
479
|
export class SQLiteBase {
|
|
495
480
|
#dbPath;
|
|
496
481
|
#db;
|
|
482
|
+
/**
|
|
483
|
+
* Open (or create) a SQLite DB at `dbPath`.
|
|
484
|
+
*
|
|
485
|
+
* v1.0.130 — multi-writer is the contract. ALL SQLiteBase consumers
|
|
486
|
+
* (SessionDB, ContentStore) may open the same on-disk dbPath from
|
|
487
|
+
* multiple processes simultaneously — that is the legitimate multi-
|
|
488
|
+
* window UX shape and the WAL handles it natively. SQLITE_BUSY on
|
|
489
|
+
* write contention is absorbed by `withRetry()` below (busy_timeout
|
|
490
|
+
* = 30000ms inside `new Database(...)`).
|
|
491
|
+
*
|
|
492
|
+
* v1.0.128 introduced a single-writer guard here as a defense against
|
|
493
|
+
* #560. That defense was an over-correction — the actual root causes
|
|
494
|
+
* of #560 were #559 (zombie MCP child accumulation) and #561 (Pi
|
|
495
|
+
* misdetection writing to the wrong DB path), both fixed in v1.0.128
|
|
496
|
+
* + v1.0.129. The single-writer guard broke legitimate multi-window
|
|
497
|
+
* users; v1.0.130 rolls it out. See
|
|
498
|
+
* docs/adr/0001-sessiondb-multi-writer.md and the v1.0.130 INVARIANT
|
|
499
|
+
* block in tests/util/db-base-platform-gate.test.ts for the
|
|
500
|
+
* regression-proof anchor (source-pin + behavioural).
|
|
501
|
+
*/
|
|
497
502
|
constructor(dbPath) {
|
|
498
503
|
const Database = loadDatabase();
|
|
499
504
|
this.#dbPath = dbPath;
|
|
500
|
-
// v1.0.128 — Issue #560 PRIMARY single-writer guard. Must claim
|
|
501
|
-
// BEFORE `new Database(...)` so a contending opener gets the clean
|
|
502
|
-
// DatabaseLockedError UX (PID + verbatim message) instead of the
|
|
503
|
-
// SQLITE_BUSY surfaced by EXCLUSIVE locking. Skip-gate via
|
|
504
|
-
// tmpdir-prefix check inside the helper — defaultDBPath() output
|
|
505
|
-
// (per-process tmp DBs) does not contend, so it never claims a lock.
|
|
506
|
-
acquireDbLock({ dbPath });
|
|
507
|
-
// v1.0.129 — single source of truth for skip decision. Both lockfile
|
|
508
|
-
// (above) and EXCLUSIVE pragma (below) MUST share the same skip-gate:
|
|
509
|
-
// tests open SessionDB twice on the same tmpdir path to exercise
|
|
510
|
-
// multi-writer scenarios; if EXCLUSIVE fires here when lockfile didn't,
|
|
511
|
-
// the second open hits SQLITE_BUSY on its FIRST pragma call inside
|
|
512
|
-
// applyWALPragmas — caused 82 test failures during v1.0.128 verification.
|
|
513
|
-
const skipExclusive = dbPath.startsWith(tmpdir());
|
|
514
505
|
cleanOrphanedWALFiles(dbPath);
|
|
515
506
|
let db;
|
|
516
507
|
try {
|
|
517
508
|
db = new Database(dbPath, { timeout: 30000 });
|
|
518
509
|
applyWALPragmas(db);
|
|
519
|
-
// v1.0.128 — Issue #560 SECONDARY defense for SQLiteBase callers (single-writer
|
|
520
|
-
// DBs like SessionDB). The .lock file (acquireDbLock above) is PRIMARY — it
|
|
521
|
-
// surfaces the conflicting PID with a clear UX message. EXCLUSIVE locking
|
|
522
|
-
// closes the narrow race window between lockfile claim + the actual
|
|
523
|
-
// `new Database(...)` open: a parallel process passing the lockfile
|
|
524
|
-
// check would still get SQLITE_BUSY from this pragma. Wrapped in try/catch —
|
|
525
|
-
// backends that don't expose locking_mode (or pragma at all) still get the
|
|
526
|
-
// lockfile floor. NOTE: NOT applied in `applyWALPragmas` because multi-writer
|
|
527
|
-
// callers (ContentStore — FTS5 shared knowledge base across sessions) rely
|
|
528
|
-
// on the default SHARED locking mode + withRetry to handle SQLITE_BUSY.
|
|
529
|
-
// Skip on tmpdir paths to match acquireDbLock's skip-gate.
|
|
530
|
-
if (!skipExclusive) {
|
|
531
|
-
try {
|
|
532
|
-
db.pragma("locking_mode = EXCLUSIVE");
|
|
533
|
-
}
|
|
534
|
-
catch { /* unsupported runtime */ }
|
|
535
|
-
}
|
|
536
510
|
}
|
|
537
511
|
catch (err) {
|
|
538
512
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -542,27 +516,17 @@ export class SQLiteBase {
|
|
|
542
516
|
try {
|
|
543
517
|
db = new Database(dbPath, { timeout: 30000 });
|
|
544
518
|
applyWALPragmas(db);
|
|
545
|
-
if (!skipExclusive) {
|
|
546
|
-
try {
|
|
547
|
-
db.pragma("locking_mode = EXCLUSIVE");
|
|
548
|
-
}
|
|
549
|
-
catch { /* unsupported runtime */ }
|
|
550
|
-
}
|
|
551
519
|
}
|
|
552
520
|
catch (retryErr) {
|
|
553
|
-
// Free the lock before bubbling — caller can never reach
|
|
554
|
-
// close()/cleanup() if the ctor throws.
|
|
555
|
-
releaseDbLock({ dbPath });
|
|
556
521
|
throw new Error(`Failed to create fresh DB after renaming corrupt file: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
|
|
557
522
|
}
|
|
558
523
|
}
|
|
559
524
|
else {
|
|
560
|
-
releaseDbLock({ dbPath });
|
|
561
525
|
throw err;
|
|
562
526
|
}
|
|
563
527
|
}
|
|
564
528
|
this.#db = db;
|
|
565
|
-
_liveDBs.
|
|
529
|
+
_liveDBs.add(this.#db);
|
|
566
530
|
this.initSchema();
|
|
567
531
|
this.prepareStatements();
|
|
568
532
|
}
|
|
@@ -578,10 +542,6 @@ export class SQLiteBase {
|
|
|
578
542
|
close() {
|
|
579
543
|
_liveDBs.delete(this.#db);
|
|
580
544
|
closeDB(this.#db);
|
|
581
|
-
// v1.0.128 — Issue #560: drop the .lock file AFTER closeDB so the
|
|
582
|
-
// WAL checkpoint inside closeDB completes while we still own the
|
|
583
|
-
// writer slot. releaseDbLock is no-op for tmpdir paths (skip-gate).
|
|
584
|
-
releaseDbLock({ dbPath: this.#dbPath });
|
|
585
545
|
}
|
|
586
546
|
withRetry(fn) {
|
|
587
547
|
return withRetry(fn);
|
|
@@ -594,9 +554,5 @@ export class SQLiteBase {
|
|
|
594
554
|
_liveDBs.delete(this.#db);
|
|
595
555
|
closeDB(this.#db);
|
|
596
556
|
deleteDBFiles(this.#dbPath);
|
|
597
|
-
// v1.0.128 — Issue #560: also drop the lockfile during cleanup. Per-
|
|
598
|
-
// process tmp DBs (defaultDBPath()) skip-gate inside the helper, so
|
|
599
|
-
// this is only a side-effect for shared on-disk content stores.
|
|
600
|
-
releaseDbLock({ dbPath: this.#dbPath });
|
|
601
557
|
}
|
|
602
558
|
}
|
|
@@ -1256,7 +1256,16 @@ function renderNarrative5Section(args) {
|
|
|
1256
1256
|
const lifetimeLegacyTokens = lifetimeEventsTokens + lifetimeRescueTokens;
|
|
1257
1257
|
const lifetimeRealTokens = realBytes?.lifetime?.totalSavedTokens ?? 0;
|
|
1258
1258
|
const lifetimeTokensWithout = Math.max(lifetimeLegacyTokens, lifetimeRealTokens);
|
|
1259
|
-
|
|
1259
|
+
// Lifetime "with" — measured when available, else legacy 0.02 fallback.
|
|
1260
|
+
// Honest definition (matches conversation bar below):
|
|
1261
|
+
// "with" = bytes_returned (what the model actually re-saw)
|
|
1262
|
+
// "without" = bytes_returned + bytes_avoided
|
|
1263
|
+
// When the schema has measurement, derive `with` from `bytes_returned/4`.
|
|
1264
|
+
const lifeRet = realBytes?.lifetime?.bytesReturned ?? 0;
|
|
1265
|
+
const lifeAv = realBytes?.lifetime?.bytesAvoided ?? 0;
|
|
1266
|
+
const lifetimeTokensWith = (lifeRet + lifeAv) > 0
|
|
1267
|
+
? Math.max(1, Math.floor(lifeRet / 4))
|
|
1268
|
+
: Math.max(1, Math.round(lifetimeTokensWithout * 0.02));
|
|
1260
1269
|
// Bytes from realBytes when present, else derive from tokens (×4 — same
|
|
1261
1270
|
// ratio Phase 8 uses everywhere). All-work bytes drives the opener tally
|
|
1262
1271
|
// + the section-3 receipt + section-4 cost example.
|
|
@@ -1328,15 +1337,32 @@ function renderNarrative5Section(args) {
|
|
|
1328
1337
|
out.push(` Without that, you'd be re-explaining everything to a blank model right now.`);
|
|
1329
1338
|
}
|
|
1330
1339
|
out.push("");
|
|
1331
|
-
// Without/With bars —
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
+
// Without/With bars — measured from real per-event bytes_returned / bytes_avoided.
|
|
1341
|
+
//
|
|
1342
|
+
// Honest definitions:
|
|
1343
|
+
// Without = bytes the model WOULD have re-seen with no filtering = bytes_returned + bytes_avoided
|
|
1344
|
+
// With = bytes the model ACTUALLY re-saw after context-mode = bytes_returned
|
|
1345
|
+
//
|
|
1346
|
+
// No fallback to heuristic. If the schema has zero signal for this
|
|
1347
|
+
// conversation (no hook ever populated bytes_avoided / bytes_returned),
|
|
1348
|
+
// the section is skipped entirely. Honesty over decoration.
|
|
1349
|
+
const realConv = realBytes?.conversation;
|
|
1350
|
+
const measuredAvoided = realConv?.bytesAvoided ?? 0;
|
|
1351
|
+
const measuredReturned = realConv?.bytesReturned ?? 0;
|
|
1352
|
+
if (measuredAvoided + measuredReturned > 0) {
|
|
1353
|
+
const convBytesWithout = measuredReturned + measuredAvoided;
|
|
1354
|
+
const convBytesWith = Math.max(1, measuredReturned);
|
|
1355
|
+
const convTokensWithout = Math.max(1, Math.floor(convBytesWithout / 4));
|
|
1356
|
+
const convTokensWith = Math.max(1, Math.floor(convBytesWith / 4));
|
|
1357
|
+
const withoutBar = dataBar(convTokensWithout, convTokensWithout, 32);
|
|
1358
|
+
const withBar = dataBar(convTokensWith, convTokensWithout, 32);
|
|
1359
|
+
const convPct = (1 - convTokensWith / convTokensWithout) * 100;
|
|
1360
|
+
const convMult = Math.max(1, Math.round(convTokensWithout / convTokensWith));
|
|
1361
|
+
out.push(` Without context-mode ${kb(convBytesWithout).padStart(8)} ${withoutBar} ${fmtNum(convTokensWithout).padStart(7)} tokens`);
|
|
1362
|
+
out.push(` With context-mode ${kb(convBytesWith).padStart(8)} ${withBar} ${fmtNum(convTokensWith).padStart(7)} tokens`);
|
|
1363
|
+
out.push(` ${convPct.toFixed(0)}% kept out of context · your AI ran ${convMult}× longer before /compact fired`);
|
|
1364
|
+
out.push("");
|
|
1365
|
+
}
|
|
1340
1366
|
// Timeline — drop-in if conversation has byDay.
|
|
1341
1367
|
if (conversation.byDay && conversation.byDay.length > 0) {
|
|
1342
1368
|
const totalConvDays = conversation.lastEventMs && conversation.firstEventMs
|