@cortexkit/opencode-magic-context 0.17.2 → 0.19.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 (130) hide show
  1. package/README.md +1 -1
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/features/magic-context/compaction-marker.d.ts +17 -0
  4. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  5. package/dist/features/magic-context/compartment-storage.d.ts +11 -0
  6. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  7. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  8. package/dist/features/magic-context/dreamer/runner.d.ts +15 -0
  9. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  10. package/dist/features/magic-context/memory/embedding-identity.d.ts +11 -0
  11. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -0
  12. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  13. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  14. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  15. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  16. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  17. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-meta-persisted.d.ts +70 -0
  19. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-meta-shared.d.ts +1 -0
  21. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  23. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  24. package/dist/features/magic-context/storage.d.ts +1 -1
  25. package/dist/features/magic-context/storage.d.ts.map +1 -1
  26. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
  27. package/dist/features/magic-context/types.d.ts +1 -0
  28. package/dist/features/magic-context/types.d.ts.map +1 -1
  29. package/dist/features/magic-context/user-memory/review-user-memories.d.ts +2 -0
  30. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/cache-busting-signals.d.ts +10 -0
  32. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -0
  33. package/dist/hooks/magic-context/command-handler.d.ts +2 -0
  34. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +50 -0
  36. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +1 -0
  38. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +7 -0
  40. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +1 -1
  42. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/compartment-runner-types.d.ts +18 -7
  46. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/compartment-runner.d.ts +7 -2
  48. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/historian-state-file.d.ts +25 -11
  51. package/dist/hooks/magic-context/historian-state-file.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/hook-handlers.d.ts +11 -4
  53. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/inject-compartments.d.ts +2 -1
  56. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/live-session-state.d.ts +3 -1
  58. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/todo-view.d.ts +102 -0
  61. package/dist/hooks/magic-context/todo-view.d.ts.map +1 -0
  62. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +11 -4
  63. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/transform-message-helpers.d.ts +22 -0
  65. package/dist/hooks/magic-context/transform-message-helpers.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +15 -1
  67. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/transform.d.ts +4 -0
  69. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  70. package/dist/index.js +1789 -711
  71. package/dist/plugin/dream-timer.d.ts.map +1 -1
  72. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  73. package/dist/plugin/rpc-handlers.d.ts +2 -1
  74. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  75. package/dist/plugin/sidebar-snapshot-cache.d.ts.map +1 -1
  76. package/dist/shared/conflict-detector.d.ts +49 -0
  77. package/dist/shared/conflict-detector.d.ts.map +1 -1
  78. package/dist/shared/conflict-fixer.d.ts +1 -1
  79. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  80. package/dist/shared/data-path.d.ts +84 -0
  81. package/dist/shared/data-path.d.ts.map +1 -1
  82. package/dist/shared/index.d.ts +1 -0
  83. package/dist/shared/index.d.ts.map +1 -1
  84. package/dist/shared/logger.d.ts +6 -0
  85. package/dist/shared/logger.d.ts.map +1 -1
  86. package/dist/shared/model-suggestion-retry.d.ts +37 -0
  87. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  88. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  89. package/dist/shared/resolve-fallbacks.d.ts +32 -0
  90. package/dist/shared/resolve-fallbacks.d.ts.map +1 -0
  91. package/dist/shared/rpc-client.d.ts +2 -1
  92. package/dist/shared/rpc-client.d.ts.map +1 -1
  93. package/dist/shared/rpc-notifications.d.ts +3 -2
  94. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  95. package/dist/shared/rpc-server.d.ts +3 -0
  96. package/dist/shared/rpc-server.d.ts.map +1 -1
  97. package/dist/shared/rpc-types.d.ts +1 -0
  98. package/dist/shared/rpc-types.d.ts.map +1 -1
  99. package/dist/shared/rpc-utils.d.ts +13 -2
  100. package/dist/shared/rpc-utils.d.ts.map +1 -1
  101. package/dist/shared/stable-json.d.ts +21 -0
  102. package/dist/shared/stable-json.d.ts.map +1 -0
  103. package/dist/shared/tag-transcript.d.ts.map +1 -1
  104. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  105. package/dist/tui/data/context-db.d.ts.map +1 -1
  106. package/package.json +1 -1
  107. package/src/shared/conflict-detector.ts +4 -4
  108. package/src/shared/conflict-fixer.test.ts +124 -0
  109. package/src/shared/conflict-fixer.ts +34 -28
  110. package/src/shared/data-path.test.ts +38 -0
  111. package/src/shared/data-path.ts +99 -0
  112. package/src/shared/index.ts +1 -0
  113. package/src/shared/logger.ts +29 -3
  114. package/src/shared/model-suggestion-retry.test.ts +251 -0
  115. package/src/shared/model-suggestion-retry.ts +194 -6
  116. package/src/shared/models-dev-cache.ts +7 -7
  117. package/src/shared/resolve-fallbacks.test.ts +136 -0
  118. package/src/shared/resolve-fallbacks.ts +76 -0
  119. package/src/shared/rpc-client.test.ts +161 -0
  120. package/src/shared/rpc-client.ts +82 -22
  121. package/src/shared/rpc-notifications.test.ts +20 -0
  122. package/src/shared/rpc-notifications.ts +9 -6
  123. package/src/shared/rpc-server.ts +42 -4
  124. package/src/shared/rpc-types.ts +1 -0
  125. package/src/shared/rpc-utils.ts +59 -3
  126. package/src/shared/stable-json.test.ts +87 -0
  127. package/src/shared/stable-json.ts +37 -0
  128. package/src/shared/tag-transcript.ts +3 -2
  129. package/src/tui/data/context-db.ts +20 -1
  130. package/src/tui/index.tsx +114 -18
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { stableStringify } from "./stable-json";
3
+
4
+ describe("stableStringify", () => {
5
+ test("primitive values match JSON.stringify", () => {
6
+ expect(stableStringify("hello")).toBe('"hello"');
7
+ expect(stableStringify(42)).toBe("42");
8
+ expect(stableStringify(true)).toBe("true");
9
+ expect(stableStringify(false)).toBe("false");
10
+ expect(stableStringify(null)).toBe("null");
11
+ });
12
+
13
+ test("undefined renders as literal string", () => {
14
+ expect(stableStringify(undefined)).toBe("undefined");
15
+ });
16
+
17
+ test("object keys sort by code-point order, not locale", () => {
18
+ // 'Z' (0x5a) sorts before 'a' (0x61) by code-point.
19
+ // localeCompare would sort 'a' before 'Z' in many locales.
20
+ // We want code-point semantics.
21
+ const input = { Z: 1, a: 2 };
22
+ expect(stableStringify(input)).toBe('{"Z":1,"a":2}');
23
+ });
24
+
25
+ test("nested objects sort recursively", () => {
26
+ const input = { b: { y: 1, x: 2 }, a: { z: 3, w: 4 } };
27
+ expect(stableStringify(input)).toBe('{"a":{"w":4,"z":3},"b":{"x":2,"y":1}}');
28
+ });
29
+
30
+ test("arrays preserve order", () => {
31
+ const input = [3, 1, 2];
32
+ expect(stableStringify(input)).toBe("[3,1,2]");
33
+ });
34
+
35
+ test("arrays of objects sort keys per element", () => {
36
+ const input = [
37
+ { b: 1, a: 2 },
38
+ { d: 3, c: 4 },
39
+ ];
40
+ expect(stableStringify(input)).toBe('[{"a":2,"b":1},{"c":4,"d":3}]');
41
+ });
42
+
43
+ test("identical objects with different key insertion order produce same string", () => {
44
+ const a = { foo: 1, bar: 2 };
45
+ const b = { bar: 2, foo: 1 };
46
+ expect(stableStringify(a)).toBe(stableStringify(b));
47
+ });
48
+
49
+ test("circular references render as marker, do not throw", () => {
50
+ const a: Record<string, unknown> = { x: 1 };
51
+ a.self = a;
52
+ expect(stableStringify(a)).toBe('{"self":"[Circular]","x":1}');
53
+ });
54
+
55
+ test("mixed cycle through array does not crash", () => {
56
+ const arr: unknown[] = [];
57
+ arr.push(arr);
58
+ expect(stableStringify(arr)).toBe('["[Circular]"]');
59
+ });
60
+
61
+ test("empty object and array", () => {
62
+ expect(stableStringify({})).toBe("{}");
63
+ expect(stableStringify([])).toBe("[]");
64
+ });
65
+
66
+ test("special string characters JSON-escaped in keys", () => {
67
+ const input = { 'with "quotes"': 1 };
68
+ expect(stableStringify(input)).toBe('{"with \\"quotes\\"":1}');
69
+ });
70
+
71
+ test("Unicode key sort by code-point, not by collation", () => {
72
+ // 'ä' (U+00E4) sorts AFTER 'z' (U+007A) by code-point.
73
+ // localeCompare in many locales would put 'ä' near 'a'.
74
+ const input = { z: 1, ä: 2 };
75
+ const result = stableStringify(input);
76
+ expect(result).toBe('{"z":1,"ä":2}');
77
+ });
78
+
79
+ test("deterministic across multiple calls", () => {
80
+ const input = { c: 3, a: 1, b: 2 };
81
+ const first = stableStringify(input);
82
+ const second = stableStringify(input);
83
+ const third = stableStringify(input);
84
+ expect(first).toBe(second);
85
+ expect(second).toBe(third);
86
+ });
87
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Process-local deterministic JSON serialization for JSON-like plain
3
+ * objects. Keys are sorted by code-point order (NOT locale-sensitive).
4
+ *
5
+ * Contract:
6
+ * - Stable for plain objects, arrays, primitives, and `null`.
7
+ * - `undefined` serialized as the string "undefined".
8
+ * - Circular references serialized as the string `"[Circular]"`.
9
+ * - **NOT** a canonical cross-runtime / cross-locale JSON serializer.
10
+ * Two different runtimes that disagree on `JSON.stringify` of primitives
11
+ * (none known today) would produce different output.
12
+ *
13
+ * Used for:
14
+ * - `tool_definition_measurements` fingerprint hashing
15
+ * - `pending_compaction_marker_state` CAS comparison
16
+ *
17
+ * If a future use case needs true canonical JSON (e.g. cross-process
18
+ * signing), build a separate utility — do NOT widen this contract.
19
+ */
20
+ export function stableStringify(value: unknown, seen = new WeakSet<object>()): string {
21
+ if (value === undefined) return "undefined";
22
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? String(value);
23
+ if (seen.has(value)) return '"[Circular]"';
24
+ seen.add(value);
25
+ if (Array.isArray(value)) {
26
+ return `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
27
+ }
28
+ // Code-point sort (NOT localeCompare). Stable across runtimes/locales.
29
+ const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => {
30
+ if (a < b) return -1;
31
+ if (a > b) return 1;
32
+ return 0;
33
+ });
34
+ return `{${entries
35
+ .map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child, seen)}`)
36
+ .join(",")}}`;
37
+ }
@@ -159,9 +159,10 @@ export function tagTranscript(
159
159
  const messageId = message.info.id;
160
160
 
161
161
  let textOrdinal = 0;
162
+ const parts = message.parts;
162
163
 
163
- for (let partIndex = 0; partIndex < message.parts.length; partIndex += 1) {
164
- const part = message.parts[partIndex];
164
+ for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
165
+ const part = parts[partIndex];
165
166
  if (part === undefined) continue;
166
167
 
167
168
  if (part.kind === "text") {
@@ -10,6 +10,7 @@ import type { RpcNotificationMessage, SidebarSnapshot, StatusDetail } from "../.
10
10
  export type { SidebarSnapshot, StatusDetail };
11
11
 
12
12
  let rpcClient: MagicContextRpcClient | null = null;
13
+ let lastReceivedNotificationId = 0;
13
14
 
14
15
  function getStorageDir(): string {
15
16
  // Plugin v0.16+ uses the shared cortexkit/magic-context path so OpenCode
@@ -30,6 +31,7 @@ export function initRpcClient(directory: string): void {
30
31
  export function closeRpc(): void {
31
32
  rpcClient?.reset();
32
33
  rpcClient = null;
34
+ lastReceivedNotificationId = 0;
33
35
  }
34
36
 
35
37
  const EMPTY_SNAPSHOT: SidebarSnapshot = {
@@ -103,9 +105,19 @@ function recallSidebarSnapshot(sessionId: string, fallback: SidebarSnapshot): Si
103
105
  stickySidebarCache.delete(sessionId);
104
106
  return fallback;
105
107
  }
108
+ if (!hasInFlightEvidence(fallback)) {
109
+ stickySidebarCache.delete(sessionId);
110
+ return fallback;
111
+ }
106
112
  return cached.snapshot;
107
113
  }
108
114
 
115
+ function hasInFlightEvidence(snapshot: SidebarSnapshot): boolean {
116
+ return (
117
+ snapshot.compartmentInProgress || snapshot.historianRunning || snapshot.pendingOpsCount > 0
118
+ );
119
+ }
120
+
109
121
  /** Fetch sidebar snapshot from the server via RPC. */
110
122
  export async function loadSidebarSnapshot(
111
123
  sessionId: string,
@@ -225,8 +237,15 @@ export async function consumeTuiMessages(): Promise<TuiMessage[]> {
225
237
  try {
226
238
  const result = await rpcClient.call<{ messages: RpcNotificationMessage[] }>(
227
239
  "pending-notifications",
240
+ { lastReceivedId: lastReceivedNotificationId },
228
241
  );
229
- return (result.messages ?? []).map((m) => ({
242
+ const messages = result.messages ?? [];
243
+ for (const message of messages) {
244
+ if (message.id > lastReceivedNotificationId) {
245
+ lastReceivedNotificationId = message.id;
246
+ }
247
+ }
248
+ return messages.map((m) => ({
230
249
  type: m.type,
231
250
  payload: m.payload,
232
251
  sessionId: m.sessionId,
package/src/tui/index.tsx CHANGED
@@ -453,6 +453,111 @@ function showStatusDialog(api: TuiPluginApi) {
453
453
  })
454
454
  }
455
455
 
456
+ /**
457
+ * Register Magic Context command palette entries, preferring the v1.14.42+
458
+ * `keymap.registerLayer` API and falling back to the legacy
459
+ * `api.command.register` for older hosts.
460
+ *
461
+ * The `keymap.registerLayer` shape uses `name`/`title`/`run`/`namespace`
462
+ * (see `@opencode-ai/plugin/tui` types) and is what the host's own legacy
463
+ * command-shim translates into. Calling it directly skips the deprecation
464
+ * warning and works without depending on the (now-deprecated) `api.command`
465
+ * namespace existing at all.
466
+ *
467
+ * Version coverage:
468
+ * 1.14.0–1.14.41 — `api.command.register` only
469
+ * 1.14.42–1.14.43 — both surfaces broken (api.command removed, keymap landed
470
+ * but with bugs); plugins crash on init either way
471
+ * 1.14.44+ — `api.keymap.registerLayer` canonical, `api.command` shim
472
+ */
473
+ function registerCommandPaletteEntries(api: TuiPluginApi): void {
474
+ type ApiAny = {
475
+ keymap?: {
476
+ registerLayer?: (layer: {
477
+ commands: Array<Record<string, unknown>>
478
+ bindings: Array<Record<string, unknown>>
479
+ }) => unknown
480
+ }
481
+ command?: {
482
+ register?: (cb: () => Array<Record<string, unknown>>) => unknown
483
+ }
484
+ }
485
+ const apiAny = api as unknown as ApiAny
486
+
487
+ if (typeof apiAny.keymap?.registerLayer === "function") {
488
+ // Audit Finding #2 hardening: even when registerLayer exists as a
489
+ // function, the underlying keymap implementation in OpenCode TUI
490
+ // 1.14.42-1.14.43 can throw at call time. Without the try-catch the
491
+ // `return` below would propagate the throw and the legacy
492
+ // `command.register` fallback path (~20 lines down) would be
493
+ // unreachable. The cost is one debug log on the rare broken-TUI
494
+ // build; the benefit is that older command.register-only TUIs
495
+ // running alongside a partially-broken keymap surface still get
496
+ // their command palette entries.
497
+ try {
498
+ apiAny.keymap.registerLayer({
499
+ commands: [
500
+ {
501
+ namespace: "palette",
502
+ name: "magic-context.status",
503
+ title: "Magic Context: Status",
504
+ category: "Magic Context",
505
+ run() {
506
+ showStatusDialog(api)
507
+ },
508
+ },
509
+ {
510
+ namespace: "palette",
511
+ name: "magic-context.recomp",
512
+ title: "Magic Context: Recomp",
513
+ category: "Magic Context",
514
+ run() {
515
+ showRecompDialog(api)
516
+ },
517
+ },
518
+ ],
519
+ bindings: [],
520
+ })
521
+ return
522
+ } catch (err) {
523
+ console.debug(
524
+ "[magic-context-tui] keymap.registerLayer threw; falling back to command.register",
525
+ err,
526
+ )
527
+ // Fall through to legacy registration.
528
+ }
529
+ }
530
+
531
+ if (typeof apiAny.command?.register === "function") {
532
+ apiAny.command.register(() => [
533
+ {
534
+ title: "Magic Context: Status",
535
+ value: "magic-context.status",
536
+ category: "Magic Context",
537
+ onSelect() {
538
+ showStatusDialog(api)
539
+ },
540
+ },
541
+ {
542
+ title: "Magic Context: Recomp",
543
+ value: "magic-context.recomp",
544
+ category: "Magic Context",
545
+ onSelect() {
546
+ showRecompDialog(api)
547
+ },
548
+ },
549
+ ])
550
+ return
551
+ }
552
+
553
+ // Neither API surface is present. The TUI host can still load — we only
554
+ // lose the command palette entry points. The sidebar (registered above
555
+ // via api.slots.register) remains visible. Status/Recomp are still
556
+ // reachable through the server-side `/ctx-status` and `/ctx-recomp`
557
+ // slash commands, which the server handler bridges to the TUI dialogs
558
+ // via RPC.
559
+ }
560
+
456
561
  const tui: TuiPlugin = async (api, _options, meta) => {
457
562
  // Initialize RPC client for server communication
458
563
  const directory = api.state.path.directory ?? ""
@@ -465,24 +570,15 @@ const tui: TuiPlugin = async (api, _options, meta) => {
465
570
  // are registered server-side so there's only one /ctx-* registration).
466
571
  // The server detects TUI mode and sends dialog requests via RPC instead
467
572
  // of sendIgnoredMessage.
468
- api.command.register(() => [
469
- {
470
- title: "Magic Context: Status",
471
- value: "magic-context.status",
472
- category: "Magic Context",
473
- onSelect() {
474
- showStatusDialog(api)
475
- },
476
- },
477
- {
478
- title: "Magic Context: Recomp",
479
- value: "magic-context.recomp",
480
- category: "Magic Context",
481
- onSelect() {
482
- showRecompDialog(api)
483
- },
484
- },
485
- ])
573
+ //
574
+ // OpenCode 1.14.42 removed `api.command.register` entirely
575
+ // (anomalyco/opencode#26053). A later patch (1.14.44+) reinstated it as
576
+ // a deprecated shim that translates to `api.keymap.registerLayer`. To
577
+ // work across all hosts (1.14.0–1.14.41 with command-only, the broken
578
+ // 1.14.42–1.14.43, and 1.14.44+ where both exist), we prefer
579
+ // `api.keymap.registerLayer` and fall back to `api.command.register`
580
+ // only when keymap is missing.
581
+ registerCommandPaletteEntries(api)
486
582
 
487
583
  // Poll for server→TUI messages: toasts and dialog requests.
488
584
  // Single poller because consumeTuiMessages() is destructive (deletes consumed rows).