@cortexkit/opencode-magic-context 0.15.1 → 0.15.3

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 (33) hide show
  1. package/README.md +12 -6
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/cli.js +12 -2
  4. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  5. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  6. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  7. package/dist/features/magic-context/resolve-subagent-fallback.d.ts +40 -0
  8. package/dist/features/magic-context/resolve-subagent-fallback.d.ts.map +1 -0
  9. package/dist/features/magic-context/storage-db.d.ts +20 -0
  10. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  11. package/dist/features/magic-context/storage-meta-persisted.d.ts +7 -0
  12. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  15. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  16. package/dist/features/magic-context/storage.d.ts +1 -1
  17. package/dist/features/magic-context/storage.d.ts.map +1 -1
  18. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  19. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  21. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  23. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  25. package/dist/index.js +207 -64
  26. package/dist/shared/bounded-session-map.d.ts +45 -0
  27. package/dist/shared/bounded-session-map.d.ts.map +1 -0
  28. package/dist/shared/conflict-detector.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/shared/bounded-session-map.test.ts +97 -0
  31. package/src/shared/bounded-session-map.ts +84 -0
  32. package/src/shared/conflict-detector.test.ts +189 -0
  33. package/src/shared/conflict-detector.ts +68 -7
package/dist/index.js CHANGED
@@ -147829,7 +147829,15 @@ function readUserCompaction() {
147829
147829
  }
147830
147830
  function checkDcpPlugin(directory) {
147831
147831
  const plugins = collectPluginEntries(directory);
147832
- return plugins.some((p) => p.includes("opencode-dcp"));
147832
+ return plugins.some((p) => matchesPackageName(p, DCP_PACKAGE_NAMES));
147833
+ }
147834
+ function matchesPackageName(entry, canonicalNames) {
147835
+ if (entry.startsWith("file:") || entry.startsWith("http:") || entry.startsWith("https:") || entry.startsWith("/") || entry.startsWith("./") || entry.startsWith("../")) {
147836
+ return false;
147837
+ }
147838
+ const lastAt = entry.lastIndexOf("@");
147839
+ const nameOnly = lastAt > 0 ? entry.slice(0, lastAt) : entry;
147840
+ return canonicalNames.has(nameOnly);
147833
147841
  }
147834
147842
  function collectPluginEntries(directory) {
147835
147843
  const plugins = [];
@@ -147862,7 +147870,7 @@ function checkOmoHooks(directory) {
147862
147870
  anthropicRecovery: false
147863
147871
  };
147864
147872
  const plugins = collectPluginEntries(directory);
147865
- const hasOmo = plugins.some((p) => p.includes("oh-my-opencode") || p.includes("oh-my-openagent") || p.includes("@code-yeongyu/"));
147873
+ const hasOmo = plugins.some((p) => matchesPackageName(p, OMO_PACKAGE_NAMES));
147866
147874
  if (!hasOmo)
147867
147875
  return result;
147868
147876
  const disabledHooks = readOmoDisabledHooks(directory);
@@ -147914,9 +147922,12 @@ function formatConflictShort(result) {
147914
147922
  return lines.join(`
147915
147923
  `);
147916
147924
  }
147925
+ var DCP_PACKAGE_NAMES, OMO_PACKAGE_NAMES;
147917
147926
  var init_conflict_detector = __esm(() => {
147918
147927
  init_jsonc_parser();
147919
147928
  init_opencode_config_dir();
147929
+ DCP_PACKAGE_NAMES = new Set(["@tarquinen/opencode-dcp"]);
147930
+ OMO_PACKAGE_NAMES = new Set(["oh-my-opencode", "oh-my-openagent"]);
147920
147931
  });
147921
147932
 
147922
147933
  // src/plugin/conflict-warning-hook.ts
@@ -149981,7 +149992,9 @@ function byteSize(value) {
149981
149992
  return encoder.encode(value).length;
149982
149993
  }
149983
149994
  function stripTagPrefix(value) {
149984
- return value.replace(TAG_PREFIX_REGEX, "");
149995
+ let stripped = value.replace(MALFORMED_TAG_PREFIX_REGEX, "");
149996
+ stripped = stripped.replace(TAG_PREFIX_REGEX, "");
149997
+ return stripped;
149985
149998
  }
149986
149999
  function prependTag(tagId, value) {
149987
150000
  const stripped = stripTagPrefix(value);
@@ -149993,10 +150006,11 @@ function isThinkingPart(part) {
149993
150006
  const candidate = part;
149994
150007
  return candidate.type === "thinking" || candidate.type === "reasoning";
149995
150008
  }
149996
- var encoder, TAG_PREFIX_REGEX;
150009
+ var encoder, TAG_PREFIX_REGEX, MALFORMED_TAG_PREFIX_REGEX;
149997
150010
  var init_tag_content_primitives = __esm(() => {
149998
150011
  encoder = new TextEncoder;
149999
150012
  TAG_PREFIX_REGEX = /^(?:\u00A7\d+\u00A7\s*)+/;
150013
+ MALFORMED_TAG_PREFIX_REGEX = /^(?:\u00A7\d+">\u00A7(?:\d+\u00A7)?\s*)+/;
150000
150014
  });
150001
150015
 
150002
150016
  // src/hooks/magic-context/tag-part-guards.ts
@@ -150397,6 +150411,7 @@ function runMigrations(db) {
150397
150411
  var MIGRATIONS;
150398
150412
  var init_migrations = __esm(() => {
150399
150413
  init_logger();
150414
+ init_storage_db();
150400
150415
  MIGRATIONS = [
150401
150416
  {
150402
150417
  version: 1,
@@ -150563,6 +150578,13 @@ var init_migrations = __esm(() => {
150563
150578
  END;
150564
150579
  `);
150565
150580
  }
150581
+ },
150582
+ {
150583
+ version: 5,
150584
+ description: "One-shot heal of NULL session_meta columns",
150585
+ up: (db) => {
150586
+ healAllNullColumns(db);
150587
+ }
150566
150588
  }
150567
150589
  ];
150568
150590
  });
@@ -150639,12 +150661,10 @@ function initializeDatabase(db) {
150639
150661
  updated_at INTEGER NOT NULL
150640
150662
  );
150641
150663
 
150642
- CREATE TABLE IF NOT EXISTS session_notes (
150643
- id INTEGER PRIMARY KEY AUTOINCREMENT,
150644
- session_id TEXT NOT NULL,
150645
- content TEXT NOT NULL,
150646
- created_at INTEGER NOT NULL
150647
- );
150664
+ -- session_notes and smart_notes were merged into the unified notes table
150665
+ -- by migration v1 (see features/magic-context/migrations.ts). The old tables
150666
+ -- are never recreated; fresh DBs create only notes, upgraded DBs have
150667
+ -- their old tables migrated and dropped by the migration runner.
150648
150668
 
150649
150669
  CREATE TABLE IF NOT EXISTS memories (
150650
150670
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -150708,20 +150728,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
150708
150728
  );
150709
150729
  CREATE INDEX IF NOT EXISTS idx_dream_runs_project ON dream_runs(project_path, finished_at DESC);
150710
150730
 
150711
- CREATE TABLE IF NOT EXISTS smart_notes (
150712
- id INTEGER PRIMARY KEY AUTOINCREMENT,
150713
- project_path TEXT NOT NULL,
150714
- content TEXT NOT NULL,
150715
- surface_condition TEXT NOT NULL,
150716
- status TEXT NOT NULL DEFAULT 'pending',
150717
- created_session_id TEXT,
150718
- created_at INTEGER NOT NULL,
150719
- updated_at INTEGER NOT NULL,
150720
- last_checked_at INTEGER,
150721
- ready_at INTEGER,
150722
- ready_reason TEXT
150723
- );
150724
- CREATE INDEX IF NOT EXISTS idx_smart_notes_project_status ON smart_notes(project_path, status);
150731
+ -- (smart_notes: see note above; merged into unified notes table by migration v1)
150725
150732
 
150726
150733
  CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
150727
150734
  content,
@@ -150821,7 +150828,6 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
150821
150828
  CREATE INDEX IF NOT EXISTS idx_session_facts_session ON session_facts(session_id);
150822
150829
  CREATE INDEX IF NOT EXISTS idx_recomp_compartments_session ON recomp_compartments(session_id);
150823
150830
  CREATE INDEX IF NOT EXISTS idx_recomp_facts_session ON recomp_facts(session_id);
150824
- CREATE INDEX IF NOT EXISTS idx_session_notes_session ON session_notes(session_id);
150825
150831
  CREATE INDEX IF NOT EXISTS idx_memories_project_status_category ON memories(project_path, status, category);
150826
150832
  CREATE INDEX IF NOT EXISTS idx_memories_project_status_expires ON memories(project_path, status, expires_at);
150827
150833
  CREATE INDEX IF NOT EXISTS idx_memories_project_category_hash ON memories(project_path, category, normalized_hash);
@@ -150867,6 +150873,8 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
150867
150873
  ensureColumn(db, "session_meta", "recomp_partial_range_end", "INTEGER DEFAULT 0");
150868
150874
  ensureColumn(db, "session_meta", "detected_context_limit", "INTEGER DEFAULT 0");
150869
150875
  ensureColumn(db, "session_meta", "needs_emergency_recovery", "INTEGER DEFAULT 0");
150876
+ }
150877
+ function healAllNullColumns(db) {
150870
150878
  healNullTextColumns(db);
150871
150879
  healNullIntegerColumns(db);
150872
150880
  healMissingMemoryBlockIds(db);
@@ -151294,6 +151302,14 @@ function recordOverflowDetected(db, sessionId, reportedLimit) {
151294
151302
  }
151295
151303
  })();
151296
151304
  }
151305
+ function recordDetectedContextLimit(db, sessionId, reportedLimit) {
151306
+ if (!(reportedLimit > 0))
151307
+ return;
151308
+ db.transaction(() => {
151309
+ ensureSessionMetaRow(db, sessionId);
151310
+ db.prepare("UPDATE session_meta SET detected_context_limit = ? WHERE session_id = ?").run(reportedLimit, sessionId);
151311
+ })();
151312
+ }
151297
151313
  function clearEmergencyRecovery(db, sessionId) {
151298
151314
  db.transaction(() => {
151299
151315
  ensureSessionMetaRow(db, sessionId);
@@ -151354,6 +151370,25 @@ var init_storage_meta_persisted = __esm(() => {
151354
151370
  init_storage_meta_shared();
151355
151371
  });
151356
151372
 
151373
+ // src/features/magic-context/resolve-subagent-fallback.ts
151374
+ function resolveIsSubagentFromOpenCodeDb(sessionId) {
151375
+ try {
151376
+ return withReadOnlySessionDb((openCodeDb) => {
151377
+ const row = openCodeDb.prepare("SELECT parent_id FROM session WHERE id = ?").get(sessionId);
151378
+ if (!row)
151379
+ return null;
151380
+ return typeof row.parent_id === "string" && row.parent_id.length > 0;
151381
+ });
151382
+ } catch (error48) {
151383
+ log(`[magic-context] resolveIsSubagentFromOpenCodeDb failed for ${sessionId}:`, error48);
151384
+ return null;
151385
+ }
151386
+ }
151387
+ var init_resolve_subagent_fallback = __esm(() => {
151388
+ init_read_session_db();
151389
+ init_logger();
151390
+ });
151391
+
151357
151392
  // src/features/magic-context/storage-meta-session.ts
151358
151393
  function getOrCreateSessionMeta(db, sessionId) {
151359
151394
  const result = db.prepare("SELECT session_id, last_response_time, cache_ttl, counter, last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent, last_context_percentage, last_input_tokens, times_execute_threshold_reached, compartment_in_progress, system_prompt_hash, system_prompt_tokens, conversation_tokens, tool_call_tokens, cleared_reasoning_through_tag FROM session_meta WHERE session_id = ?").get(sessionId);
@@ -151361,7 +151396,14 @@ function getOrCreateSessionMeta(db, sessionId) {
151361
151396
  return toSessionMeta(result);
151362
151397
  }
151363
151398
  const defaults = getDefaultSessionMeta(sessionId);
151399
+ const fallbackSubagent = resolveIsSubagentFromOpenCodeDb(sessionId);
151400
+ if (fallbackSubagent === true) {
151401
+ defaults.isSubagent = true;
151402
+ }
151364
151403
  ensureSessionMetaRow(db, sessionId);
151404
+ if (fallbackSubagent === true) {
151405
+ db.prepare("UPDATE session_meta SET is_subagent = 1 WHERE session_id = ?").run(sessionId);
151406
+ }
151365
151407
  return defaults;
151366
151408
  }
151367
151409
  function updateSessionMeta(db, sessionId, updates) {
@@ -151409,6 +151451,7 @@ function clearSession(db, sessionId) {
151409
151451
  var init_storage_meta_session = __esm(() => {
151410
151452
  init_compression_depth_storage();
151411
151453
  init_message_index();
151454
+ init_resolve_subagent_fallback();
151412
151455
  init_storage_meta_shared();
151413
151456
  });
151414
151457
 
@@ -152428,6 +152471,51 @@ var init_constants = __esm(() => {
152428
152471
  };
152429
152472
  });
152430
152473
 
152474
+ // src/shared/bounded-session-map.ts
152475
+ class BoundedSessionMap {
152476
+ maxEntries;
152477
+ store = new Map;
152478
+ constructor(maxEntries) {
152479
+ if (!Number.isFinite(maxEntries) || maxEntries < 1) {
152480
+ throw new Error(`BoundedSessionMap: maxEntries must be >= 1, got ${maxEntries}`);
152481
+ }
152482
+ this.maxEntries = maxEntries;
152483
+ }
152484
+ get(sessionId) {
152485
+ const value = this.store.get(sessionId);
152486
+ if (value === undefined)
152487
+ return;
152488
+ this.store.delete(sessionId);
152489
+ this.store.set(sessionId, value);
152490
+ return value;
152491
+ }
152492
+ peek(sessionId) {
152493
+ return this.store.get(sessionId);
152494
+ }
152495
+ has(sessionId) {
152496
+ return this.store.has(sessionId);
152497
+ }
152498
+ set(sessionId, value) {
152499
+ if (this.store.has(sessionId)) {
152500
+ this.store.delete(sessionId);
152501
+ } else if (this.store.size >= this.maxEntries) {
152502
+ const oldest = this.store.keys().next().value;
152503
+ if (oldest !== undefined)
152504
+ this.store.delete(oldest);
152505
+ }
152506
+ this.store.set(sessionId, value);
152507
+ }
152508
+ delete(sessionId) {
152509
+ return this.store.delete(sessionId);
152510
+ }
152511
+ clear() {
152512
+ this.store.clear();
152513
+ }
152514
+ get size() {
152515
+ return this.store.size;
152516
+ }
152517
+ }
152518
+
152431
152519
  // src/hooks/magic-context/temporal-awareness.ts
152432
152520
  function formatGap(seconds) {
152433
152521
  if (!Number.isFinite(seconds) || seconds < TEMPORAL_AWARENESS_THRESHOLD_SECONDS) {
@@ -152774,7 +152862,7 @@ function findFirstTextPart(parts) {
152774
152862
  function isDroppedPlaceholder(text) {
152775
152863
  return /^\[dropped \u00A7\d+\u00A7\]$/.test(text.trim());
152776
152864
  }
152777
- var injectionCache, CONSTRAINT_KEYWORDS;
152865
+ var INJECTION_CACHE_MAX = 100, injectionCache, CONSTRAINT_KEYWORDS;
152778
152866
  var init_inject_compartments = __esm(() => {
152779
152867
  init_compartment_storage();
152780
152868
  init_constants();
@@ -152783,7 +152871,7 @@ var init_inject_compartments = __esm(() => {
152783
152871
  init_read_session_db();
152784
152872
  init_read_session_formatting();
152785
152873
  init_temporal_awareness();
152786
- injectionCache = new Map;
152874
+ injectionCache = new BoundedSessionMap(INJECTION_CACHE_MAX);
152787
152875
  CONSTRAINT_KEYWORDS = /\b(must|never|always|cannot|should not|must not)\b/i;
152788
152876
  });
152789
152877
 
@@ -153309,6 +153397,28 @@ function generatePartId(timestampMs, counter = 0n) {
153309
153397
  function getOpenCodeDbPath3() {
153310
153398
  return join13(getDataDir(), "opencode", "opencode.db");
153311
153399
  }
153400
+ function isOpenCodeSchemaCompatible(db, dbPath) {
153401
+ if (cachedSchemaCompatible?.path === dbPath) {
153402
+ return cachedSchemaCompatible.compatible;
153403
+ }
153404
+ try {
153405
+ const messageCols = new Set(db.prepare("PRAGMA table_info(message)").all().map((r) => r.name ?? "").filter((n) => n.length > 0));
153406
+ const partCols = new Set(db.prepare("PRAGMA table_info(part)").all().map((r) => r.name ?? "").filter((n) => n.length > 0));
153407
+ const missingMessage = REQUIRED_MESSAGE_COLUMNS.filter((c) => !messageCols.has(c));
153408
+ const missingPart = REQUIRED_PART_COLUMNS.filter((c) => !partCols.has(c));
153409
+ if (missingMessage.length > 0 || missingPart.length > 0) {
153410
+ log(`[magic-context] compaction-marker: OpenCode DB schema missing required columns ` + `(message: [${missingMessage.join(", ")}], part: [${missingPart.join(", ")}]). ` + `Marker injection disabled for this process. ` + `This usually means OpenCode was updated and magic-context is out of date.`);
153411
+ cachedSchemaCompatible = { path: dbPath, compatible: false };
153412
+ return false;
153413
+ }
153414
+ cachedSchemaCompatible = { path: dbPath, compatible: true };
153415
+ return true;
153416
+ } catch (error48) {
153417
+ log(`[magic-context] compaction-marker: schema probe failed: ${error48 instanceof Error ? error48.message : String(error48)}. ` + `Marker injection disabled until next process restart.`);
153418
+ cachedSchemaCompatible = { path: dbPath, compatible: false };
153419
+ return false;
153420
+ }
153421
+ }
153312
153422
  function getWritableOpenCodeDb() {
153313
153423
  const dbPath = getOpenCodeDbPath3();
153314
153424
  if (cachedWriteDb?.path === dbPath) {
@@ -153327,18 +153437,15 @@ function getWritableOpenCodeDb() {
153327
153437
  }
153328
153438
  function findBoundaryUserMessage(sessionId, endOrdinal) {
153329
153439
  const db = getWritableOpenCodeDb();
153330
- const rows = db.prepare("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId);
153331
- const filtered = rows.filter((row) => {
153332
- try {
153333
- const info = JSON.parse(row.data);
153334
- return !(info.summary === true && info.finish === "stop");
153335
- } catch {
153336
- return true;
153337
- }
153338
- });
153440
+ const rows = db.prepare(`SELECT id, time_created, data
153441
+ FROM message
153442
+ WHERE session_id = ?
153443
+ AND NOT (COALESCE(json_extract(data, '$.summary'), 0) = 1
153444
+ AND COALESCE(json_extract(data, '$.finish'), '') = 'stop')
153445
+ ORDER BY time_created ASC, id ASC
153446
+ LIMIT ?`).all(sessionId, endOrdinal);
153339
153447
  let bestMatch = null;
153340
- for (let i = 0;i < filtered.length && i < endOrdinal; i++) {
153341
- const row = filtered[i];
153448
+ for (const row of rows) {
153342
153449
  try {
153343
153450
  const info = JSON.parse(row.data);
153344
153451
  if (info.role === "user") {
@@ -153349,12 +153456,15 @@ function findBoundaryUserMessage(sessionId, endOrdinal) {
153349
153456
  return bestMatch;
153350
153457
  }
153351
153458
  function injectCompactionMarker(args) {
153459
+ const db = getWritableOpenCodeDb();
153460
+ if (!isOpenCodeSchemaCompatible(db, getOpenCodeDbPath3())) {
153461
+ return null;
153462
+ }
153352
153463
  const boundary = findBoundaryUserMessage(args.sessionId, args.endOrdinal);
153353
153464
  if (!boundary) {
153354
153465
  log(`[magic-context] compaction-marker: no user message found at or before ordinal ${args.endOrdinal}`);
153355
153466
  return null;
153356
153467
  }
153357
- const db = getWritableOpenCodeDb();
153358
153468
  const boundaryTime = boundary.timeCreated;
153359
153469
  const summaryMsgId = generateMessageId(boundaryTime + 1, 1n);
153360
153470
  const compactionPartId = generatePartId(boundaryTime, 1n);
@@ -153405,10 +153515,19 @@ function removeCompactionMarker(state) {
153405
153515
  return false;
153406
153516
  }
153407
153517
  }
153408
- var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", cachedWriteDb = null;
153518
+ var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", cachedWriteDb = null, REQUIRED_MESSAGE_COLUMNS, REQUIRED_PART_COLUMNS, cachedSchemaCompatible = null;
153409
153519
  var init_compaction_marker = __esm(() => {
153410
153520
  init_data_path();
153411
153521
  init_logger();
153522
+ REQUIRED_MESSAGE_COLUMNS = ["id", "session_id", "time_created", "time_updated", "data"];
153523
+ REQUIRED_PART_COLUMNS = [
153524
+ "id",
153525
+ "message_id",
153526
+ "session_id",
153527
+ "time_created",
153528
+ "time_updated",
153529
+ "data"
153530
+ ];
153412
153531
  });
153413
153532
 
153414
153533
  // src/hooks/magic-context/compaction-marker-manager.ts
@@ -164485,7 +164604,8 @@ async function runDream(args) {
164485
164604
  smartNotesPending: result.smartNotesPending,
164486
164605
  memoryChanges: persistedMemoryChanges
164487
164606
  });
164488
- const hasSuccessfulTask = result.tasks.some((t) => !t.error && t.name !== "smart-notes");
164607
+ const POST_TASK_NAMES = new Set(["smart-notes", "user memories", "key files"]);
164608
+ const hasSuccessfulTask = result.tasks.some((t) => !t.error && !POST_TASK_NAMES.has(t.name));
164489
164609
  if (hasSuccessfulTask) {
164490
164610
  setDreamState(args.db, `last_dream_at:${args.projectIdentity}`, String(result.finishedAt));
164491
164611
  setDreamState(args.db, "last_dream_at", String(result.finishedAt));
@@ -169307,7 +169427,14 @@ async function runPostTransformPhase(args) {
169307
169427
  }
169308
169428
  }
169309
169429
  }
169310
- const pendingUserTurnReminder = getPersistedStickyTurnReminder(args.db, args.sessionId);
169430
+ const pendingUserTurnReminder = args.fullFeatureMode ? getPersistedStickyTurnReminder(args.db, args.sessionId) : null;
169431
+ if (!args.fullFeatureMode && isCacheBustingPass) {
169432
+ const stale = getPersistedStickyTurnReminder(args.db, args.sessionId);
169433
+ if (stale) {
169434
+ clearPersistedStickyTurnReminder(args.db, args.sessionId);
169435
+ sessionLog(args.sessionId, "sticky turn reminder cleared \u2014 subagent should not have this state (cache-busting pass)");
169436
+ }
169437
+ }
169311
169438
  if (pendingUserTurnReminder) {
169312
169439
  if (args.hasRecentReduceCall && isCacheBustingPass) {
169313
169440
  clearPersistedStickyTurnReminder(args.db, args.sessionId);
@@ -169438,7 +169565,8 @@ function createNudgePlacementStore(db) {
169438
169565
 
169439
169566
  // src/hooks/magic-context/transform.ts
169440
169567
  init_storage_meta_persisted();
169441
- var messageTokensBySession = new Map;
169568
+ var MESSAGE_TOKENS_CACHE_MAX = 100;
169569
+ var messageTokensBySession = new BoundedSessionMap(MESSAGE_TOKENS_CACHE_MAX);
169442
169570
  function getMessageTokensCache(sessionId) {
169443
169571
  let cache = messageTokensBySession.get(sessionId);
169444
169572
  if (!cache) {
@@ -169628,11 +169756,11 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so ma
169628
169756
  }
169629
169757
  }
169630
169758
  logTransformTiming(sessionId, "emergencyRecoveryBlock", tFirstPass);
169759
+ const projectIdentity = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
169631
169760
  let pendingCompartmentInjection = null;
169632
169761
  if (fullFeatureMode) {
169633
169762
  const tInj = performance.now();
169634
- const projectPath = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
169635
- pendingCompartmentInjection = prepareCompartmentInjection(db, sessionId, messages, isCacheBusting, projectPath, deps.memoryConfig?.injectionBudgetTokens, deps.experimentalTemporalAwareness);
169763
+ pendingCompartmentInjection = prepareCompartmentInjection(db, sessionId, messages, isCacheBusting, projectIdentity, deps.memoryConfig?.injectionBudgetTokens, deps.experimentalTemporalAwareness);
169636
169764
  logTransformTiming(sessionId, "prepareCompartmentInjection", tInj);
169637
169765
  }
169638
169766
  let targets = new Map;
@@ -169663,7 +169791,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so ma
169663
169791
  hasRecentReduceCall = result.hasRecentReduceCall;
169664
169792
  const hadPriorCommitState = deps.commitSeenLastPass?.has(sessionId) ?? false;
169665
169793
  const sawCommitLastPass = deps.commitSeenLastPass?.get(sessionId) ?? false;
169666
- if (hadPriorCommitState && result.hasRecentCommit && !sawCommitLastPass) {
169794
+ if (fullFeatureMode && hadPriorCommitState && result.hasRecentCommit && !sawCommitLastPass) {
169667
169795
  onNoteTrigger(db, sessionId, "commit_detected");
169668
169796
  }
169669
169797
  deps.commitSeenLastPass?.set(sessionId, result.hasRecentCommit);
@@ -169747,7 +169875,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so ma
169747
169875
  messages,
169748
169876
  pendingCompartmentInjection,
169749
169877
  fallbackModelId,
169750
- projectPath: deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined,
169878
+ projectPath: projectIdentity,
169751
169879
  injectionBudgetTokens: deps.memoryConfig?.injectionBudgetTokens,
169752
169880
  getNotificationParams: rawGetNotifParams ? () => rawGetNotifParams(sessionId) : undefined,
169753
169881
  cacheAlreadyBusting: isCacheBusting || schedulerDecisionEarly === "execute",
@@ -170032,6 +170160,14 @@ function createEventHandler2(deps) {
170032
170160
  if (!detection.isOverflow) {
170033
170161
  return;
170034
170162
  }
170163
+ const sessionMeta = getOrCreateSessionMeta(deps.db, errInfo.sessionID);
170164
+ if (sessionMeta.isSubagent) {
170165
+ if (typeof detection.reportedLimit === "number" && detection.reportedLimit > 0) {
170166
+ recordDetectedContextLimit(deps.db, errInfo.sessionID, detection.reportedLimit);
170167
+ }
170168
+ sessionLog(errInfo.sessionID, `overflow detected on subagent: reportedLimit=${detection.reportedLimit ?? "unknown"} pattern=${detection.matchedPattern ?? "n/a"} \u2014 recorded limit only (subagents cannot run historian)`);
170169
+ return;
170170
+ }
170035
170171
  const existing = getOverflowState(deps.db, errInfo.sessionID);
170036
170172
  recordOverflowDetected(deps.db, errInfo.sessionID, detection.reportedLimit);
170037
170173
  sessionLog(errInfo.sessionID, `overflow detected via session.error: reportedLimit=${detection.reportedLimit ?? "unknown"} pattern=${detection.matchedPattern ?? "n/a"} (previousRecovery=${existing.needsEmergencyRecovery})`);
@@ -170061,9 +170197,17 @@ function createEventHandler2(deps) {
170061
170197
  const detection = detectOverflow(info.error);
170062
170198
  if (detection.isOverflow) {
170063
170199
  try {
170064
- recordOverflowDetected(deps.db, info.sessionID, detection.reportedLimit);
170065
- sessionLog(info.sessionID, `overflow detected via message.updated: reportedLimit=${detection.reportedLimit ?? "unknown"} pattern=${detection.matchedPattern ?? "n/a"}`);
170066
- deps.onSessionCacheInvalidated?.(info.sessionID);
170200
+ const metaForOverflow = getOrCreateSessionMeta(deps.db, info.sessionID);
170201
+ if (metaForOverflow.isSubagent) {
170202
+ if (typeof detection.reportedLimit === "number" && detection.reportedLimit > 0) {
170203
+ recordDetectedContextLimit(deps.db, info.sessionID, detection.reportedLimit);
170204
+ }
170205
+ sessionLog(info.sessionID, `overflow detected on subagent via message.updated: reportedLimit=${detection.reportedLimit ?? "unknown"} pattern=${detection.matchedPattern ?? "n/a"} \u2014 recorded limit only`);
170206
+ } else {
170207
+ recordOverflowDetected(deps.db, info.sessionID, detection.reportedLimit);
170208
+ sessionLog(info.sessionID, `overflow detected via message.updated: reportedLimit=${detection.reportedLimit ?? "unknown"} pattern=${detection.matchedPattern ?? "n/a"}`);
170209
+ deps.onSessionCacheInvalidated?.(info.sessionID);
170210
+ }
170067
170211
  } catch (error48) {
170068
170212
  sessionLog(info.sessionID, "event message.updated overflow persistence failed:", error48);
170069
170213
  }
@@ -170504,7 +170648,10 @@ function createToolExecuteAfterHook(args) {
170504
170648
  const todoArgs = typedInput.args;
170505
170649
  const todos = todoArgs?.todos;
170506
170650
  if (Array.isArray(todos) && todos.length > 0 && todos.every((t) => t.status === "completed" || t.status === "cancelled")) {
170507
- onNoteTrigger(args.db, typedInput.sessionID, "todos_complete");
170651
+ const sessionMeta = getOrCreateSessionMeta(args.db, typedInput.sessionID);
170652
+ if (!sessionMeta.isSubagent) {
170653
+ onNoteTrigger(args.db, typedInput.sessionID, "todos_complete");
170654
+ }
170508
170655
  }
170509
170656
  }
170510
170657
  if (typedInput.tool === "ctx_note") {
@@ -170539,7 +170686,7 @@ Use \`ctx_memory\` to manage cross-session project memories. Write new memories
170539
170686
  - Discovered a non-obvious build/test command \u2192 \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
170540
170687
  - Learned a constraint the hard way \u2192 \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
170541
170688
  Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
170542
- Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start=N end=M>\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
170689
+ Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
170543
170690
  **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
170544
170691
  - Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
170545
170692
  - Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
@@ -170552,15 +170699,14 @@ NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
170552
170699
  NEVER drop user messages \u2014 they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
170553
170700
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
170554
170701
  Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
170555
- var BASE_INTRO_NO_REDUCE = (dropToolStructure) => `Messages and tool outputs are tagged with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).
170556
- Use \`ctx_note\` for deferred intentions \u2014 things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
170702
+ var BASE_INTRO_NO_REDUCE = (dropToolStructure) => `Use \`ctx_note\` for deferred intentions \u2014 things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
170557
170703
  Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
170558
170704
  **Save to memory proactively**: If you spent multiple turns finding something (a file path, a DB location, a config pattern, a workaround), save it with \`ctx_memory\` so future sessions don't repeat the search. Examples:
170559
170705
  - Found a project's source code path after searching \u2192 \`ctx_memory(action="write", category="ENVIRONMENT", content="OpenCode source is at ~/Work/OSS/opencode")\`
170560
170706
  - Discovered a non-obvious build/test command \u2192 \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
170561
170707
  - Learned a constraint the hard way \u2192 \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
170562
170708
  Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
170563
- Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start=N end=M>\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
170709
+ Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
170564
170710
  **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
170565
170711
  - Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
170566
170712
  - Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
@@ -170795,7 +170941,7 @@ function createSystemPromptHashHandler(deps) {
170795
170941
  sessionLog(sessionId, `injected ${detectedAgent ?? "generic"} guidance into system prompt (ctxReduce=${effectiveCtxReduceEnabled}, subagent=${isSubagentSession})`);
170796
170942
  }
170797
170943
  const isCacheBusting = deps.flushedSessions.has(sessionId);
170798
- if (shouldInjectDocs) {
170944
+ if (shouldInjectDocs && !isSubagentSession) {
170799
170945
  const hasCached = cachedDocsBySession.has(sessionId);
170800
170946
  if (!hasCached || isCacheBusting) {
170801
170947
  const docsContent = readProjectDocs(deps.directory);
@@ -170811,7 +170957,7 @@ function createSystemPromptHashHandler(deps) {
170811
170957
  output.system.push(docsBlock);
170812
170958
  }
170813
170959
  }
170814
- if (deps.experimentalUserMemories) {
170960
+ if (deps.experimentalUserMemories && !isSubagentSession) {
170815
170961
  const hasCachedProfile = cachedUserProfileBySession.has(sessionId);
170816
170962
  if (!hasCachedProfile || isCacheBusting) {
170817
170963
  const memories = getActiveUserMemories(deps.db);
@@ -170833,7 +170979,7 @@ ${items}
170833
170979
  output.system.push(profileBlock);
170834
170980
  }
170835
170981
  }
170836
- if (deps.experimentalPinKeyFiles) {
170982
+ if (deps.experimentalPinKeyFiles && !isSubagentSession) {
170837
170983
  const hasCachedKeyFiles = cachedKeyFilesBySession.has(sessionId);
170838
170984
  if (!hasCachedKeyFiles || isCacheBusting) {
170839
170985
  const keyFileEntries = getKeyFiles(deps.db, sessionId);
@@ -170924,13 +171070,10 @@ ${sections.join(`
170924
171070
  if (systemContent.length === 0)
170925
171071
  return;
170926
171072
  const currentHash = new Bun.CryptoHasher("md5").update(systemContent).digest("hex");
170927
- let sessionMeta;
170928
- try {
170929
- sessionMeta = getOrCreateSessionMeta(deps.db, sessionId);
170930
- } catch (error48) {
170931
- sessionLog(sessionId, "system-prompt-hash DB update failed:", error48);
171073
+ if (!sessionMetaEarly) {
170932
171074
  return;
170933
171075
  }
171076
+ const sessionMeta = sessionMetaEarly;
170934
171077
  const previousHash = sessionMeta.systemPromptHash;
170935
171078
  if (previousHash !== "" && previousHash !== "0" && previousHash !== currentHash) {
170936
171079
  sessionLog(sessionId, `system prompt hash changed: ${previousHash} \u2192 ${currentHash} (len=${systemContent.length}), triggering flush`);
@@ -171743,7 +171886,7 @@ init_read_session_chunk();
171743
171886
  import { tool } from "@opencode-ai/plugin";
171744
171887
 
171745
171888
  // src/tools/ctx-expand/constants.ts
171746
- var CTX_EXPAND_DESCRIPTION = "Decompress a compartment range to see the original conversation transcript. " + "Use start/end from <compartment start=N end=M> attributes. " + "Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.";
171889
+ var CTX_EXPAND_DESCRIPTION = "Decompress a compartment range to see the original conversation transcript. " + 'Use start/end from <compartment start="N" end="M"> attributes. ' + "Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.";
171747
171890
  var CTX_EXPAND_TOKEN_BUDGET = 15000;
171748
171891
 
171749
171892
  // src/tools/ctx-expand/tools.ts
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Bounded LRU map keyed by session id.
3
+ *
4
+ * Rationale: magic-context maintains several module-scope Maps that track
5
+ * per-session state (prepared injection cache, per-message token cache, etc.).
6
+ * These are cleared on the `session.deleted` event, but sessions that are
7
+ * never explicitly deleted — because OpenCode crashed, the user force-quit,
8
+ * the session was archived rather than deleted, or the session simply outlived
9
+ * the plugin process's interest in it — leak entries for the lifetime of the
10
+ * plugin process.
11
+ *
12
+ * In long-running OpenCode instances with thousands of sessions over time,
13
+ * an unbounded `Map<sessionId, LargeObject>` can retain tens of megabytes
14
+ * indefinitely. A session-scoped LRU with a generous cap (e.g. 100) covers
15
+ * any realistic working-set of active sessions a user actually cares about,
16
+ * while evicting cold session ids that will either never return or be
17
+ * rebuilt from durable SQLite state on their next transform pass.
18
+ *
19
+ * Implementation notes:
20
+ * - Built on `Map` which preserves insertion order. On every `set`/`get`
21
+ * touch we delete+reinsert to move the key to the tail (most-recent).
22
+ * - Eviction drops the oldest entry (first in iteration order).
23
+ * - The cached value type is generic — callers decide what per-session state
24
+ * to store. For injection/token state, all three properties of the cached
25
+ * object are safe to throw away: they are either recomputable from the
26
+ * messages array on the next pass, or reloadable from SQLite.
27
+ */
28
+ export declare class BoundedSessionMap<V> {
29
+ private readonly maxEntries;
30
+ private readonly store;
31
+ constructor(maxEntries: number);
32
+ get(sessionId: string): V | undefined;
33
+ /**
34
+ * Peek without touching recency — useful for `has`-style checks that
35
+ * should not rearrange LRU order. Use sparingly; `get` is the normal
36
+ * access path.
37
+ */
38
+ peek(sessionId: string): V | undefined;
39
+ has(sessionId: string): boolean;
40
+ set(sessionId: string, value: V): void;
41
+ delete(sessionId: string): boolean;
42
+ clear(): void;
43
+ get size(): number;
44
+ }
45
+ //# sourceMappingURL=bounded-session-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded-session-map.d.ts","sourceRoot":"","sources":["../../src/shared/bounded-session-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,iBAAiB,CAAC,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;gBAElC,UAAU,EAAE,MAAM;IAO9B,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IASrC;;;;OAIG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAItC,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAYtC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlC,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"conflict-detector.d.ts","sourceRoot":"","sources":["../../src/shared/conflict-detector.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,WAAW,EAAE,OAAO,CAAC;IACrB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE;QACP,cAAc,EAAE,OAAO,CAAC;QACxB,eAAe,EAAE,OAAO,CAAC;QACzB,SAAS,EAAE,OAAO,CAAC;QACnB,uBAAuB,EAAE,OAAO,CAAC;QACjC,uBAAuB,EAAE,OAAO,CAAC;QACjC,oBAAoB,EAAE,OAAO,CAAC;KACjC,CAAC;CACL;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAyDjE;AA0LD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAWlE"}
1
+ {"version":3,"file":"conflict-detector.d.ts","sourceRoot":"","sources":["../../src/shared/conflict-detector.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,WAAW,EAAE,OAAO,CAAC;IACrB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE;QACP,cAAc,EAAE,OAAO,CAAC;QACxB,eAAe,EAAE,OAAO,CAAC;QACzB,SAAS,EAAE,OAAO,CAAC;QACnB,uBAAuB,EAAE,OAAO,CAAC;QACjC,uBAAuB,EAAE,OAAO,CAAC;QACjC,oBAAoB,EAAE,OAAO,CAAC;KACjC,CAAC;CACL;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAyDjE;AAuPD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAWlE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",