@cortexkit/opencode-magic-context 0.15.7 → 0.16.1

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 (204) hide show
  1. package/README.md +42 -16
  2. package/dist/agents/magic-context-prompt.d.ts +2 -13
  3. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  4. package/dist/config/schema/magic-context.d.ts +67 -4
  5. package/dist/config/schema/magic-context.d.ts.map +1 -1
  6. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  7. package/dist/features/magic-context/compaction.d.ts +1 -1
  8. package/dist/features/magic-context/compaction.d.ts.map +1 -1
  9. package/dist/features/magic-context/compartment-storage.d.ts +1 -1
  10. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  11. package/dist/features/magic-context/compression-depth-storage.d.ts +1 -1
  12. package/dist/features/magic-context/compression-depth-storage.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/lease.d.ts +1 -1
  14. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/queue.d.ts +8 -3
  16. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/runner.d.ts +1 -1
  18. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/scheduler.d.ts +1 -1
  20. package/dist/features/magic-context/dreamer/scheduler.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +1 -1
  22. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/storage-dream-state.d.ts +1 -1
  24. package/dist/features/magic-context/dreamer/storage-dream-state.d.ts.map +1 -1
  25. package/dist/features/magic-context/git-commits/indexer.d.ts +1 -1
  26. package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
  27. package/dist/features/magic-context/git-commits/search-git-commits.d.ts +1 -1
  28. package/dist/features/magic-context/git-commits/search-git-commits.d.ts.map +1 -1
  29. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts +1 -1
  30. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  31. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts +1 -1
  32. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  33. package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
  34. package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
  35. package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
  36. package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
  37. package/dist/features/magic-context/key-files/storage-key-files.d.ts +1 -1
  38. package/dist/features/magic-context/key-files/storage-key-files.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/embedding-backfill.d.ts +1 -1
  40. package/dist/features/magic-context/memory/embedding-backfill.d.ts.map +1 -1
  41. package/dist/features/magic-context/memory/embedding-cache.d.ts +1 -1
  42. package/dist/features/magic-context/memory/embedding-cache.d.ts.map +1 -1
  43. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  44. package/dist/features/magic-context/memory/embedding.d.ts +1 -1
  45. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  46. package/dist/features/magic-context/memory/normalize-hash.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  48. package/dist/features/magic-context/memory/promotion.d.ts +1 -1
  49. package/dist/features/magic-context/memory/promotion.d.ts.map +1 -1
  50. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +1 -1
  51. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  52. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -1
  53. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  54. package/dist/features/magic-context/memory/storage-memory.d.ts +1 -1
  55. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  56. package/dist/features/magic-context/message-index.d.ts +1 -1
  57. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  58. package/dist/features/magic-context/migrations.d.ts +1 -1
  59. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  60. package/dist/features/magic-context/mock-database.d.ts +1 -1
  61. package/dist/features/magic-context/mock-database.d.ts.map +1 -1
  62. package/dist/features/magic-context/plugin-messages.d.ts +1 -1
  63. package/dist/features/magic-context/plugin-messages.d.ts.map +1 -1
  64. package/dist/features/magic-context/search.d.ts +1 -1
  65. package/dist/features/magic-context/search.d.ts.map +1 -1
  66. package/dist/features/magic-context/sidekick/agent.d.ts +2 -1
  67. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  68. package/dist/features/magic-context/sidekick/core.d.ts +38 -0
  69. package/dist/features/magic-context/sidekick/core.d.ts.map +1 -0
  70. package/dist/features/magic-context/storage-db.d.ts +20 -1
  71. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  72. package/dist/features/magic-context/storage-meta-persisted.d.ts +1 -1
  73. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  74. package/dist/features/magic-context/storage-meta-session.d.ts +1 -1
  75. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  76. package/dist/features/magic-context/storage-meta-shared.d.ts +1 -1
  77. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  78. package/dist/features/magic-context/storage-notes.d.ts +1 -1
  79. package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
  80. package/dist/features/magic-context/storage-ops.d.ts +1 -1
  81. package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
  82. package/dist/features/magic-context/storage-source.d.ts +1 -1
  83. package/dist/features/magic-context/storage-source.d.ts.map +1 -1
  84. package/dist/features/magic-context/storage-tags.d.ts +17 -1
  85. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  86. package/dist/features/magic-context/tagger.d.ts +1 -1
  87. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  88. package/dist/features/magic-context/user-memory/review-user-memories.d.ts +1 -1
  89. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  90. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +1 -1
  91. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
  94. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/command-handler.d.ts +1 -1
  96. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +1 -1
  98. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-prompt.d.ts +1 -0
  100. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +1 -1
  102. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts +1 -1
  104. package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +1 -0
  106. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/compartment-runner-types.d.ts +1 -1
  109. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
  111. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/execute-flush.d.ts +1 -1
  113. package/dist/hooks/magic-context/execute-flush.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/execute-status.d.ts +1 -1
  115. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/historian-state-file.d.ts +29 -0
  117. package/dist/hooks/magic-context/historian-state-file.d.ts.map +1 -0
  118. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/inject-compartments.d.ts +1 -1
  120. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  121. package/dist/hooks/magic-context/note-nudger.d.ts +1 -1
  122. package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
  123. package/dist/hooks/magic-context/nudge-placement-store.d.ts +1 -1
  124. package/dist/hooks/magic-context/nudge-placement-store.d.ts.map +1 -1
  125. package/dist/hooks/magic-context/read-session-chunk.d.ts +39 -0
  126. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  127. package/dist/hooks/magic-context/read-session-db.d.ts +1 -1
  128. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  129. package/dist/hooks/magic-context/read-session-raw.d.ts +1 -1
  130. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  131. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  132. package/dist/hooks/magic-context/system-prompt-hash.d.ts +6 -5
  133. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  134. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  135. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  136. package/dist/index.js +8284 -8166
  137. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  138. package/dist/plugin/messages-transform.d.ts +1 -1
  139. package/dist/plugin/rpc-handlers.d.ts +4 -0
  140. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  141. package/dist/plugin/tool-registry.d.ts.map +1 -1
  142. package/dist/shared/conflict-detector.d.ts.map +1 -1
  143. package/dist/shared/data-path.d.ts +22 -0
  144. package/dist/shared/data-path.d.ts.map +1 -1
  145. package/dist/shared/harness.d.ts +43 -0
  146. package/dist/shared/harness.d.ts.map +1 -0
  147. package/dist/shared/rpc-notifications.d.ts +4 -2
  148. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  149. package/dist/shared/sqlite-helpers.d.ts +16 -0
  150. package/dist/shared/sqlite-helpers.d.ts.map +1 -0
  151. package/dist/shared/sqlite.d.ts +55 -0
  152. package/dist/shared/sqlite.d.ts.map +1 -0
  153. package/dist/shared/subagent-runner.d.ts +202 -0
  154. package/dist/shared/subagent-runner.d.ts.map +1 -0
  155. package/dist/shared/tag-transcript.d.ts +66 -0
  156. package/dist/shared/tag-transcript.d.ts.map +1 -0
  157. package/dist/shared/transcript-opencode.d.ts +71 -0
  158. package/dist/shared/transcript-opencode.d.ts.map +1 -0
  159. package/dist/shared/transcript.d.ts +212 -0
  160. package/dist/shared/transcript.d.ts.map +1 -0
  161. package/dist/shared/tui-config.d.ts.map +1 -1
  162. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  163. package/dist/tools/ctx-memory/types.d.ts +13 -2
  164. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  165. package/dist/tools/ctx-note/tools.d.ts +8 -2
  166. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  167. package/dist/tools/ctx-reduce/tools.d.ts +1 -1
  168. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  169. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  170. package/dist/tools/ctx-search/types.d.ts +8 -2
  171. package/dist/tools/ctx-search/types.d.ts.map +1 -1
  172. package/dist/tui/data/context-db.d.ts.map +1 -1
  173. package/package.json +73 -75
  174. package/src/shared/conflict-detector.test.ts +44 -1
  175. package/src/shared/conflict-detector.ts +24 -8
  176. package/src/shared/data-path.test.ts +53 -1
  177. package/src/shared/data-path.ts +28 -0
  178. package/src/shared/harness.ts +61 -0
  179. package/src/shared/rpc-notifications.ts +11 -5
  180. package/src/shared/sqlite-helpers.ts +27 -0
  181. package/src/shared/sqlite.ts +91 -0
  182. package/src/shared/subagent-runner.ts +206 -0
  183. package/src/shared/tag-transcript.ts +541 -0
  184. package/src/shared/transcript-opencode.ts +259 -0
  185. package/src/shared/transcript.ts +226 -0
  186. package/src/shared/tui-config.ts +34 -8
  187. package/src/tui/data/context-db.ts +5 -1
  188. package/dist/cli/config-paths.d.ts +0 -11
  189. package/dist/cli/config-paths.d.ts.map +0 -1
  190. package/dist/cli/diagnostics.d.ts +0 -82
  191. package/dist/cli/diagnostics.d.ts.map +0 -1
  192. package/dist/cli/doctor.d.ts +0 -5
  193. package/dist/cli/doctor.d.ts.map +0 -1
  194. package/dist/cli/index.d.ts +0 -3
  195. package/dist/cli/index.d.ts.map +0 -1
  196. package/dist/cli/logs.d.ts +0 -22
  197. package/dist/cli/logs.d.ts.map +0 -1
  198. package/dist/cli/opencode-helpers.d.ts +0 -19
  199. package/dist/cli/opencode-helpers.d.ts.map +0 -1
  200. package/dist/cli/prompts.d.ts +0 -14
  201. package/dist/cli/prompts.d.ts.map +0 -1
  202. package/dist/cli/setup.d.ts +0 -2
  203. package/dist/cli/setup.d.ts.map +0 -1
  204. package/dist/cli.js +0 -11287
@@ -1,7 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { getCacheDir, getDataDir, getOpenCodeCacheDir, getOpenCodeStorageDir } from "./data-path";
4
+ import {
5
+ getCacheDir,
6
+ getDataDir,
7
+ getLegacyOpenCodeMagicContextStorageDir,
8
+ getMagicContextStorageDir,
9
+ getOpenCodeCacheDir,
10
+ getOpenCodeStorageDir,
11
+ } from "./data-path";
5
12
 
6
13
  const savedEnv = {
7
14
  XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
@@ -66,4 +73,49 @@ describe("data-path", () => {
66
73
  path.join(os.homedir(), ".local", "share", "opencode", "storage"),
67
74
  );
68
75
  });
76
+
77
+ test("getMagicContextStorageDir uses cortexkit/magic-context layout", () => {
78
+ // Cross-harness shared path: both OpenCode and Pi plugins read/write here,
79
+ // unlike the legacy opencode/storage/plugin/magic-context location which
80
+ // was OpenCode-specific. See ARCHITECTURE_DECISIONS memory for rationale.
81
+ expect(getMagicContextStorageDir()).toBe(
82
+ path.join(os.homedir(), ".local", "share", "cortexkit", "magic-context"),
83
+ );
84
+ });
85
+
86
+ test("getMagicContextStorageDir honors XDG_DATA_HOME", () => {
87
+ process.env.XDG_DATA_HOME = "/tmp/custom-data";
88
+ expect(getMagicContextStorageDir()).toBe(
89
+ path.join("/tmp/custom-data", "cortexkit", "magic-context"),
90
+ );
91
+ });
92
+
93
+ test("getLegacyOpenCodeMagicContextStorageDir points at the pre-cortexkit OpenCode path", () => {
94
+ // Used only for one-time migration of pre-shared-storage data into the new
95
+ // location. Must remain stable so users with legacy installs can still
96
+ // have their data migrated forward across multiple plugin upgrades.
97
+ expect(getLegacyOpenCodeMagicContextStorageDir()).toBe(
98
+ path.join(
99
+ os.homedir(),
100
+ ".local",
101
+ "share",
102
+ "opencode",
103
+ "storage",
104
+ "plugin",
105
+ "magic-context",
106
+ ),
107
+ );
108
+ });
109
+
110
+ test("legacy storage dir distinct from new shared dir even with same XDG override", () => {
111
+ // Sanity check: even when XDG_DATA_HOME points the same place, the two
112
+ // resolvers must return different paths so the migration copy doesn't
113
+ // self-overwrite.
114
+ process.env.XDG_DATA_HOME = "/tmp/test-xdg";
115
+ const legacy = getLegacyOpenCodeMagicContextStorageDir();
116
+ const shared = getMagicContextStorageDir();
117
+ expect(legacy).not.toBe(shared);
118
+ expect(legacy).toContain("opencode");
119
+ expect(shared).toContain("cortexkit");
120
+ });
69
121
  });
@@ -9,6 +9,34 @@ export function getOpenCodeStorageDir(): string {
9
9
  return path.join(getDataDir(), "opencode", "storage");
10
10
  }
11
11
 
12
+ /**
13
+ * Resolve the shared magic-context storage directory.
14
+ *
15
+ * Magic-context's own data (compartments, facts, memories, embeddings, dream
16
+ * runs, notes, etc.) lives at this path regardless of which harness loaded the
17
+ * plugin (OpenCode or Pi). This enables:
18
+ * - Shared project memories across harnesses
19
+ * - Shared embedding cache
20
+ * - Shared Dreamer runs (one per project per machine)
21
+ * - Future cross-harness session migration
22
+ *
23
+ * Layout: <XDG_DATA_HOME>/cortexkit/magic-context/
24
+ */
25
+ export function getMagicContextStorageDir(): string {
26
+ return path.join(getDataDir(), "cortexkit", "magic-context");
27
+ }
28
+
29
+ /**
30
+ * Legacy magic-context storage directory used by the OpenCode plugin before the
31
+ * shared cortexkit path. Used only for one-time migration of existing data into
32
+ * the new shared location. The legacy directory is left in place after copy so
33
+ * users can roll back if needed; manual cleanup is safe after one stable
34
+ * release.
35
+ */
36
+ export function getLegacyOpenCodeMagicContextStorageDir(): string {
37
+ return path.join(getOpenCodeStorageDir(), "plugin", "magic-context");
38
+ }
39
+
12
40
  /**
13
41
  * Resolve OpenCode's cache base directory.
14
42
  *
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Identifier for the host harness this plugin is running inside.
3
+ *
4
+ * Magic Context's SQLite database lives at a vendor-scoped path
5
+ * (`~/.local/share/cortexkit/magic-context/`) so OpenCode and Pi can share
6
+ * project memories, embedding cache, dreamer runs, and other project-scoped
7
+ * state. Session-scoped tables carry a `harness` column populated from this
8
+ * module so we can disambiguate which harness wrote each session row,
9
+ * filter by harness in the dashboard, and (eventually) migrate sessions
10
+ * between harnesses.
11
+ *
12
+ * Each plugin entry point sets this once at boot, before any DB write
13
+ * happens:
14
+ * - OpenCode plugin: relies on the default ("opencode") — no setHarness call
15
+ * needed
16
+ * - Pi plugin: calls `setHarness("pi")` before opening the database
17
+ *
18
+ * NEVER read this from configuration or session state — it is a
19
+ * boot-time constant per plugin instance. Cross-harness leakage is a
20
+ * correctness bug, not a feature.
21
+ */
22
+ export type HarnessId = "opencode" | "pi";
23
+
24
+ let currentHarness: HarnessId = "opencode";
25
+ let harnessLocked = false;
26
+
27
+ /**
28
+ * Set the harness identifier for this plugin instance. Must be called once
29
+ * at boot before any DB write happens. Subsequent calls with a different
30
+ * value throw to prevent accidental mid-session swaps that would corrupt
31
+ * the harness column and break per-harness session scoping.
32
+ *
33
+ * Calling with the same value as the current is a no-op (safe to call
34
+ * defensively).
35
+ */
36
+ export function setHarness(value: HarnessId): void {
37
+ if (harnessLocked && currentHarness !== value) {
38
+ throw new Error(
39
+ `Magic Context: harness already locked to "${currentHarness}"; cannot change to "${value}"`,
40
+ );
41
+ }
42
+ currentHarness = value;
43
+ harnessLocked = true;
44
+ }
45
+
46
+ /**
47
+ * Get the current harness identifier. Used by storage modules when
48
+ * INSERTing session-scoped rows so each row is correctly attributed.
49
+ */
50
+ export function getHarness(): HarnessId {
51
+ return currentHarness;
52
+ }
53
+
54
+ /**
55
+ * Test-only helper to reset harness state between test cases. Do NOT call
56
+ * from production code paths.
57
+ */
58
+ export function _resetHarnessForTesting(): void {
59
+ currentHarness = "opencode";
60
+ harnessLocked = false;
61
+ }
@@ -14,7 +14,11 @@ export interface RpcNotification {
14
14
  }
15
15
 
16
16
  let queue: RpcNotification[] = [];
17
- let tuiConnected = false;
17
+ // Timestamp of last drain — used to detect if TUI is actively polling.
18
+ // The TUI polls every 500ms; we consider it connected if it polled within
19
+ // the last 3 seconds (6× the poll interval, tolerates transient delays).
20
+ let lastDrainAt = 0;
21
+ const TUI_CONNECTED_WINDOW_MS = 3_000;
18
22
 
19
23
  /** Push a notification for TUI to pick up via polling. */
20
24
  export function pushNotification(
@@ -30,15 +34,17 @@ export function pushNotification(
30
34
  }
31
35
 
32
36
  /** Drain and return all pending notifications atomically.
33
- * Also marks TUI as connected since only TUI polls this. */
37
+ * Updates lastDrainAt so isTuiConnected() reflects recent activity. */
34
38
  export function drainNotifications(): RpcNotification[] {
35
- tuiConnected = true;
39
+ lastDrainAt = Date.now();
36
40
  const result = queue;
37
41
  queue = [];
38
42
  return result;
39
43
  }
40
44
 
41
- /** Whether a TUI client has connected and is polling for notifications. */
45
+ /** Whether a TUI client is actively polling for notifications.
46
+ * Returns true only if the TUI has drained within the last 3 seconds.
47
+ * This prevents stale-connected state after TUI closes or disconnects. */
42
48
  export function isTuiConnected(): boolean {
43
- return tuiConnected;
49
+ return lastDrainAt > 0 && Date.now() - lastDrainAt < TUI_CONNECTED_WINDOW_MS;
44
50
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Cross-runtime helpers that smooth over the small bun:sqlite ↔ better-sqlite3
3
+ * API differences without leaking either library into call sites.
4
+ */
5
+
6
+ import type { Database } from "./sqlite";
7
+
8
+ /**
9
+ * Close a database, ignoring errors.
10
+ *
11
+ * bun:sqlite supports `db.close(throwOnError = false)`. better-sqlite3 has
12
+ * only `db.close()` and throws on already-closed databases. This helper
13
+ * mirrors the bun "swallow errors" semantics for both runtimes — useful in
14
+ * test teardown and `finally` blocks where the caller doesn't care whether
15
+ * the close succeeded.
16
+ */
17
+ export function closeQuietly(db: Database | null | undefined): void {
18
+ if (!db) return;
19
+ // Just attempt close and swallow errors. bun:sqlite has no `open` property,
20
+ // and better-sqlite3 throws TypeError on already-closed databases — both
21
+ // are handled by the bare try/catch.
22
+ try {
23
+ db.close();
24
+ } catch {
25
+ // intentional: caller wants quiet close
26
+ }
27
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * SQLite chokepoint — runtime-detected backend selection.
3
+ *
4
+ * The same shipped plugin artifact must run under two different runtimes:
5
+ * - Bun (current OpenCode releases) → uses `bun:sqlite` (built-in, fast)
6
+ * - Node (OpenCode beta + future Pi plugin) → uses `better-sqlite3`
7
+ *
8
+ * Bun cannot load `better-sqlite3` (oven-sh/bun#4290), and Node has no
9
+ * `bun:sqlite` module. Static imports of either would crash at parse time
10
+ * in the wrong runtime, so we use dynamic imports gated by runtime detection.
11
+ *
12
+ * The Function-constructor wrapper around `import()` defeats bundler static
13
+ * analysis — without it, esbuild/bun build would try to resolve both modules
14
+ * during the bundle step, including the one that doesn't exist in the build
15
+ * runtime.
16
+ *
17
+ * Both libraries expose ~95% API parity:
18
+ * - new Database(path, { readonly?: boolean })
19
+ * - db.prepare(sql).run/get/all
20
+ * - db.exec(multistatement)
21
+ * - db.transaction(fn) → wrapped function
22
+ * - db.close()
23
+ *
24
+ * The 5% that differs (db.query, db.run, db.close(boolean), Database.open)
25
+ * is either rewritten to common-subset patterns or hidden behind the helpers
26
+ * in `./sqlite-helpers.ts`.
27
+ */
28
+
29
+ // Detect Bun via process.versions.bun. Both globalThis.Bun and
30
+ // process.versions.bun are set by the Bun runtime, but process.versions
31
+ // is a lower-level surface less likely to be sandboxed by host runtimes
32
+ // (e.g. Electron in OpenCode desktop apps that re-expose a Bun-flavored
33
+ // environment). Real Node and Electron never set this field.
34
+ const isBun = typeof process !== "undefined" && typeof process.versions?.bun === "string";
35
+
36
+ // IMPORTANT: bundler-evading dynamic imports.
37
+ //
38
+ // We can't write `await import("better-sqlite3")` directly because esbuild/bun
39
+ // would try to resolve both modules at build time, and one of them won't exist
40
+ // in the build runtime (bun:sqlite is missing in Node, better-sqlite3 isn't
41
+ // shipped in Bun-only environments). Earlier versions used
42
+ // `new Function("p", "return import(p)")("modname")` to defeat static
43
+ // analysis, but that breaks Pi's vm-based extension loader: a Function
44
+ // constructed at runtime has no module record, so `import()` inside it has
45
+ // no referrer module and Node throws "A dynamic import callback was not
46
+ // specified".
47
+ //
48
+ // The /* @vite-ignore */ + variable indirection pattern hides the specifier
49
+ // from static analyzers while keeping a real referrer module for the
50
+ // dynamic import — Pi's loader, esbuild, and bun build all accept it.
51
+ const bunSpec = "bun:" + "sqlite";
52
+ const betterSpec = "better-" + "sqlite3";
53
+ const sqliteModule = isBun
54
+ ? await import(/* @vite-ignore */ bunSpec)
55
+ : await import(/* @vite-ignore */ betterSpec);
56
+
57
+ // Different export shapes between the two libraries:
58
+ // - bun:sqlite → named export `Database`
59
+ // - better-sqlite3 → default export
60
+ const DatabaseImpl = isBun ? sqliteModule.Database : sqliteModule.default;
61
+
62
+ /**
63
+ * Database constructor compatible with both bun:sqlite and better-sqlite3.
64
+ *
65
+ * The TypeScript type intentionally references @types/better-sqlite3 because
66
+ * its definitions are richer than @types/bun's bun:sqlite types and bun:sqlite
67
+ * is a structural superset for the API surface we use. Calls written against
68
+ * this type work correctly under both runtimes at runtime.
69
+ *
70
+ * @types/better-sqlite3 uses `export = Database` (CommonJS interop), which
71
+ * surfaces in TypeScript as `import Database = require("better-sqlite3")`.
72
+ * We capture the DatabaseConstructor type from the namespace re-export.
73
+ */
74
+ import type BetterSqlite3 from "better-sqlite3";
75
+
76
+ export const Database: typeof BetterSqlite3 = DatabaseImpl;
77
+
78
+ /** Instance type alias used by helpers and storage modules. */
79
+ export type Database = BetterSqlite3.Database;
80
+
81
+ /**
82
+ * Statement instance type used for WeakMap caches throughout the codebase.
83
+ *
84
+ * We deliberately use the variadic Statement<unknown[], unknown> shape rather
85
+ * than `ReturnType<Database["prepare"]>` because the latter resolves through
86
+ * a conditional return type in @types/better-sqlite3 that confuses TypeScript
87
+ * about how many arguments .run/.get/.all accept. With this explicit type,
88
+ * cached statements accept any number of bind args (matching bun:sqlite's
89
+ * historical behavior in this codebase).
90
+ */
91
+ export type Statement = BetterSqlite3.Statement<unknown[], unknown>;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Cross-harness subagent runner abstraction.
3
+ *
4
+ * Magic Context spawns three kinds of subagents — historian, dreamer, sidekick —
5
+ * each as a child "session" with its own model/prompt/tools. OpenCode and Pi
6
+ * have very different APIs for this:
7
+ *
8
+ * - OpenCode: `client.session.create({parentID}) → client.session.prompt() →
9
+ * client.session.messages() → client.session.delete()`. The plugin runs
10
+ * in-process with the OpenCode server and uses its SDK client directly.
11
+ *
12
+ * - Pi: no in-process child-session API. Instead `pi --print --mode=json`
13
+ * spawns a non-interactive subprocess that emits structured JSON events
14
+ * and exits when the agent loop finishes. Sessions are JSONL files on
15
+ * disk, optionally addressed via `--session <path>`.
16
+ *
17
+ * The runner interface below normalizes both into the same shape so the
18
+ * actual subagent business logic (historian XML parsing, dreamer task loop,
19
+ * sidekick augmentation) can stay harness-agnostic. Each harness ships its
20
+ * own runner implementation; agents take a `SubagentRunner` as a dep instead
21
+ * of reaching for `client.session.*` directly.
22
+ *
23
+ * Step 5a (this commit) defines the contract and ships `PiSubagentRunner`.
24
+ * Step 5b will refactor the OpenCode-side spawn paths in
25
+ * `compartment-runner-historian.ts`, `dreamer/runner.ts`, and
26
+ * `sidekick/agent.ts` onto an `OpenCodeSubagentRunner` so both harnesses
27
+ * share the agent business logic instead of duplicating it. Until 5b lands,
28
+ * OpenCode keeps its existing direct `client.session.*` calls untouched —
29
+ * the runner contract is purely additive on the OpenCode side.
30
+ */
31
+
32
+ /**
33
+ * Configuration for one subagent invocation.
34
+ *
35
+ * Mirrors the union of OpenCode's `session.create` + `session.prompt` body
36
+ * fields and Pi's `--print` CLI flags, picking the shared subset that all
37
+ * three subagent kinds (historian, dreamer, sidekick) actually use today.
38
+ *
39
+ * Fields:
40
+ * - `agent`: harness-specific agent name. OpenCode looks this up in its
41
+ * agent registry (`HISTORIAN_AGENT`, `DREAMER_AGENT`, `SIDEKICK_AGENT`).
42
+ * Pi has no concept of "agent name" beyond config, so this is ignored
43
+ * on the Pi side and used only by `OpenCodeSubagentRunner`.
44
+ * - `systemPrompt`: full system prompt for this child run. Replaces (not
45
+ * appends to) any harness-default system prompt.
46
+ * - `userMessage`: the single user-turn prompt. Subagent runs are always
47
+ * one-shot — no multi-turn conversation in the child.
48
+ * - `model`: provider/model identifier in the canonical "provider/model"
49
+ * shape (e.g. "anthropic/claude-sonnet-4-7"). Each runner is responsible
50
+ * for translating to its harness's native model selection.
51
+ * - `fallbackModels`: ordered list of models to try if `model` fails. Both
52
+ * harnesses retry on transient model failures.
53
+ * - `timeoutMs`: hard cap on the child run. The runner aborts the child on
54
+ * exceeding this and returns `{ ok: false, reason: "timeout" }`.
55
+ * - `cwd`: working directory for the child. OpenCode uses this for
56
+ * `query.directory`; Pi uses it as the spawn cwd so that `--cwd`-aware
57
+ * tools see the right project root.
58
+ * - `signal`: optional AbortSignal so callers can cancel an in-flight run
59
+ * (used by dreamer's lease-renewal-aborts-on-loss path).
60
+ */
61
+ export interface SubagentRunOptions {
62
+ agent: string;
63
+ systemPrompt: string;
64
+ userMessage: string;
65
+ model?: string | undefined;
66
+ fallbackModels?: readonly string[];
67
+ timeoutMs?: number | undefined;
68
+ cwd?: string | undefined;
69
+ signal?: AbortSignal | undefined;
70
+ /**
71
+ * Pi only: explicit thinking level, passed as `--thinking <level>` to the
72
+ * Pi subprocess. OpenCode ignores this field — thinking/reasoning is
73
+ * controlled via `variant` in the OpenCode agent config instead.
74
+ *
75
+ * Required when the configured historian/dreamer model supports reasoning
76
+ * (e.g. github-copilot/gpt-5.4) because Pi's own default resolution may
77
+ * pick a value the provider rejects. Set to "off" to disable thinking for
78
+ * speed (local models), or "medium"/"high" for better quality.
79
+ */
80
+ thinkingLevel?: string | undefined;
81
+
82
+ /**
83
+ * Optional progress callback. The runner invokes it for milestone events
84
+ * during the run: spawn, first event received, terminal stop reason
85
+ * detected, child exit. Used by historian/dreamer/sidekick to write
86
+ * lifecycle entries to the magic-context.log without polluting the
87
+ * normal stdout stream.
88
+ *
89
+ * Implementations must be non-throwing and fast — they're called on the
90
+ * runner's hot path. Errors are swallowed.
91
+ */
92
+ onProgress?: (event: SubagentProgressEvent) => void;
93
+ }
94
+
95
+ /**
96
+ * Progress events emitted by a runner during a run. Distinct from the final
97
+ * `SubagentRunResult` — these are mid-run milestones plus (optionally) every
98
+ * raw event the underlying harness emits, so callers can write a complete
99
+ * trace to the log when diagnosing hangs.
100
+ *
101
+ * Categories:
102
+ * - `spawned` / `child_exit` / `stderr` — process lifecycle.
103
+ * - `first_event` — convenience: first event received from the child, useful
104
+ * for measuring auth/network warmup time.
105
+ * - `terminal` — runner detected the final assistant turn (Pi: assistant
106
+ * message_end with terminal stopReason and no toolCall; OpenCode: SDK
107
+ * `agent_end` equivalent).
108
+ * - `raw_event` — every parsed event from the harness's structured output
109
+ * stream (Pi NDJSON / OpenCode SDK events). Emitted unconditionally so
110
+ * debug logs can capture the full timeline. The `event` payload is
111
+ * harness-shaped — callers should treat it as `unknown` and log it raw.
112
+ */
113
+ export type SubagentProgressEvent =
114
+ | { type: "spawned"; argv: readonly string[]; pid: number | undefined }
115
+ | { type: "first_event"; eventType: string; ms: number }
116
+ | {
117
+ type: "raw_event";
118
+ eventType: string | undefined;
119
+ event: unknown;
120
+ ms: number;
121
+ }
122
+ | {
123
+ type: "terminal";
124
+ stopReason: string | undefined;
125
+ textLength: number;
126
+ hasToolCall: boolean;
127
+ ms: number;
128
+ }
129
+ | { type: "stderr"; chunk: string }
130
+ | { type: "child_exit"; code: number | null; signal: string | null; ms: number };
131
+
132
+ /**
133
+ * Result of one subagent invocation.
134
+ *
135
+ * The runner contract is "fail soft": transient errors, timeouts, model
136
+ * failures, and aborts all surface as `{ ok: false, reason }` with a
137
+ * machine-readable reason and a human-readable message. Throwing is
138
+ * reserved for programmer errors (bad arguments, missing dependencies)
139
+ * that the agent code couldn't have caused.
140
+ *
141
+ * Fields:
142
+ * - `ok`: true iff the child produced a final assistant message.
143
+ * - `assistantText`: concatenated text content from the final assistant
144
+ * message, with leading/trailing whitespace trimmed. Empty string if the
145
+ * child finished but produced no text (rare — usually means the model
146
+ * only emitted tool calls and we didn't follow up).
147
+ * - `reason`: failure category, one of:
148
+ * - `"timeout"`: hit `timeoutMs` before the child finished
149
+ * - `"abort"`: caller's `signal` was triggered
150
+ * - `"model_failed"`: every configured model + fallback returned an error
151
+ * - `"spawn_failed"`: subprocess couldn't start (Pi only — binary missing,
152
+ * permission denied, etc.)
153
+ * - `"non_zero_exit"`: child exited unsuccessfully before a final answer
154
+ * - `"no_assistant"`: child completed without a final assistant message
155
+ * - `"parse_failed"`: child emitted output we couldn't parse (Pi only —
156
+ * JSON malformed or unexpected event ordering)
157
+ * - `error`: human-readable detail; safe to log, may include stack info.
158
+ * - `durationMs`: wall-clock time from runner-call to runner-return.
159
+ * - `meta`: optional harness-specific debug payload. Currently unused; left
160
+ * here so the OpenCode runner can surface the child session ID for log
161
+ * correlation when Step 5b lands.
162
+ */
163
+ export type SubagentRunResult =
164
+ | {
165
+ ok: true;
166
+ assistantText: string;
167
+ durationMs: number;
168
+ meta?: Record<string, unknown>;
169
+ }
170
+ | {
171
+ ok: false;
172
+ reason:
173
+ | "timeout"
174
+ | "abort"
175
+ | "model_failed"
176
+ | "spawn_failed"
177
+ | "non_zero_exit"
178
+ | "no_assistant"
179
+ | "parse_failed";
180
+ error: string;
181
+ durationMs: number;
182
+ meta?: Record<string, unknown>;
183
+ };
184
+
185
+ /**
186
+ * Abstract runner contract.
187
+ *
188
+ * Each harness ships a single instance — the OpenCode plugin wires
189
+ * `OpenCodeSubagentRunner` and the Pi plugin wires `PiSubagentRunner` in
190
+ * its `extension` boot path. Agent code (historian, dreamer, sidekick)
191
+ * receives the runner as a dep and never reaches for harness-specific
192
+ * client APIs directly.
193
+ */
194
+ export interface SubagentRunner {
195
+ /** Human-readable harness name, for logging (`"opencode"` or `"pi"`). */
196
+ readonly harness: string;
197
+
198
+ /**
199
+ * Run one subagent invocation to completion.
200
+ *
201
+ * Always resolves with a `SubagentRunResult` — never throws for
202
+ * runtime/transport/model failures. Throwing is reserved for caller
203
+ * misuse (e.g. missing required option fields).
204
+ */
205
+ run(options: SubagentRunOptions): Promise<SubagentRunResult>;
206
+ }