agent-mockingbird 0.0.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 (227) hide show
  1. package/.agents/skills/btca-cli/SKILL.md +64 -0
  2. package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
  5. package/.env.example +36 -0
  6. package/.githooks/pre-commit +33 -0
  7. package/.github/workflows/ci.yml +309 -0
  8. package/.opencode/bun.lock +18 -0
  9. package/.opencode/package.json +5 -0
  10. package/.opencode/tools/agent_type_manager.ts +100 -0
  11. package/.opencode/tools/config_manager.ts +87 -0
  12. package/.opencode/tools/cron_manager.ts +145 -0
  13. package/.opencode/tools/memory_get.ts +43 -0
  14. package/.opencode/tools/memory_remember.ts +53 -0
  15. package/.opencode/tools/memory_search.ts +48 -0
  16. package/AGENTS.md +126 -0
  17. package/MEMORY.md +2 -0
  18. package/README.md +451 -0
  19. package/THIRD_PARTY_NOTICES.md +11 -0
  20. package/agent-mockingbird.config.example.json +135 -0
  21. package/apps/server/package.json +32 -0
  22. package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
  23. package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
  24. package/apps/server/src/backend/agents/openclawImport.ts +797 -0
  25. package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
  26. package/apps/server/src/backend/agents/service.ts +10 -0
  27. package/apps/server/src/backend/config/example-config.test.ts +20 -0
  28. package/apps/server/src/backend/config/orchestration.ts +243 -0
  29. package/apps/server/src/backend/config/policy.ts +158 -0
  30. package/apps/server/src/backend/config/schema.test.ts +15 -0
  31. package/apps/server/src/backend/config/schema.ts +391 -0
  32. package/apps/server/src/backend/config/semantic.test.ts +34 -0
  33. package/apps/server/src/backend/config/semantic.ts +149 -0
  34. package/apps/server/src/backend/config/service.test.ts +75 -0
  35. package/apps/server/src/backend/config/service.ts +207 -0
  36. package/apps/server/src/backend/config/smoke.ts +77 -0
  37. package/apps/server/src/backend/config/store.test.ts +123 -0
  38. package/apps/server/src/backend/config/store.ts +581 -0
  39. package/apps/server/src/backend/config/testFixtures.ts +5 -0
  40. package/apps/server/src/backend/config/types.ts +56 -0
  41. package/apps/server/src/backend/contracts/events.ts +320 -0
  42. package/apps/server/src/backend/contracts/runtime.ts +111 -0
  43. package/apps/server/src/backend/cron/executor.ts +435 -0
  44. package/apps/server/src/backend/cron/repository.ts +170 -0
  45. package/apps/server/src/backend/cron/service.ts +660 -0
  46. package/apps/server/src/backend/cron/storage.ts +92 -0
  47. package/apps/server/src/backend/cron/types.ts +138 -0
  48. package/apps/server/src/backend/cron/utils.ts +351 -0
  49. package/apps/server/src/backend/db/client.ts +20 -0
  50. package/apps/server/src/backend/db/migrate.ts +40 -0
  51. package/apps/server/src/backend/db/repository.ts +1762 -0
  52. package/apps/server/src/backend/db/schema.ts +113 -0
  53. package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
  54. package/apps/server/src/backend/db/wipe.ts +13 -0
  55. package/apps/server/src/backend/defaults.ts +32 -0
  56. package/apps/server/src/backend/env.ts +48 -0
  57. package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
  58. package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
  59. package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
  60. package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
  61. package/apps/server/src/backend/heartbeat/service.ts +176 -0
  62. package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
  63. package/apps/server/src/backend/heartbeat/state.ts +167 -0
  64. package/apps/server/src/backend/heartbeat/types.ts +54 -0
  65. package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
  66. package/apps/server/src/backend/http/boundedQueue.ts +92 -0
  67. package/apps/server/src/backend/http/parsers.ts +40 -0
  68. package/apps/server/src/backend/http/router.ts +61 -0
  69. package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
  70. package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
  71. package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
  72. package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
  73. package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
  74. package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
  75. package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
  76. package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
  77. package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
  78. package/apps/server/src/backend/http/routes/index.ts +101 -0
  79. package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
  80. package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
  81. package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
  82. package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
  83. package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
  84. package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
  85. package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
  86. package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
  87. package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
  88. package/apps/server/src/backend/http/schemas.ts +64 -0
  89. package/apps/server/src/backend/http/sse.ts +144 -0
  90. package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
  91. package/apps/server/src/backend/logging/logger.ts +64 -0
  92. package/apps/server/src/backend/mcp/service.ts +326 -0
  93. package/apps/server/src/backend/memory/cli.ts +170 -0
  94. package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
  95. package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
  96. package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
  97. package/apps/server/src/backend/memory/qmdPort.ts +61 -0
  98. package/apps/server/src/backend/memory/records.test.ts +66 -0
  99. package/apps/server/src/backend/memory/records.ts +229 -0
  100. package/apps/server/src/backend/memory/service.ts +2012 -0
  101. package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
  102. package/apps/server/src/backend/memory/types.ts +104 -0
  103. package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
  104. package/apps/server/src/backend/opencode/client.ts +98 -0
  105. package/apps/server/src/backend/opencode/models.ts +41 -0
  106. package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
  107. package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
  108. package/apps/server/src/backend/paths.ts +57 -0
  109. package/apps/server/src/backend/prompts/service.ts +100 -0
  110. package/apps/server/src/backend/queue/queue.test.ts +189 -0
  111. package/apps/server/src/backend/queue/service.ts +177 -0
  112. package/apps/server/src/backend/queue/types.ts +39 -0
  113. package/apps/server/src/backend/run/service.ts +576 -0
  114. package/apps/server/src/backend/run/storage.ts +47 -0
  115. package/apps/server/src/backend/run/types.ts +44 -0
  116. package/apps/server/src/backend/runtime/errors.ts +61 -0
  117. package/apps/server/src/backend/runtime/index.ts +72 -0
  118. package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
  119. package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
  120. package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
  121. package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
  122. package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
  123. package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
  124. package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
  125. package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
  126. package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
  127. package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
  128. package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
  129. package/apps/server/src/backend/skills/service.ts +442 -0
  130. package/apps/server/src/backend/workspace/resolve.ts +27 -0
  131. package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
  132. package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
  133. package/apps/server/src/cli/runtime-assets.mjs +269 -0
  134. package/apps/server/src/cli/runtime-assets.test.ts +52 -0
  135. package/apps/server/src/cli/runtime-layout.mjs +75 -0
  136. package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
  137. package/apps/server/src/cli/standaloneBuild.ts +19 -0
  138. package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
  139. package/apps/server/src/index.ts +178 -0
  140. package/apps/server/tsconfig.json +12 -0
  141. package/backlog.md +5 -0
  142. package/bin/agent-mockingbird +2522 -0
  143. package/bin/runtime-layout.mjs +75 -0
  144. package/build-bin.ts +34 -0
  145. package/build-cli.mjs +37 -0
  146. package/build.ts +40 -0
  147. package/bun-env.d.ts +11 -0
  148. package/bun.lock +888 -0
  149. package/bunfig.toml +2 -0
  150. package/components.json +21 -0
  151. package/config.json +130 -0
  152. package/deploy/RELEASE_INSTALL.md +112 -0
  153. package/deploy/docker-compose.yml +42 -0
  154. package/deploy/systemd/README.md +46 -0
  155. package/deploy/systemd/agent-mockingbird.service +28 -0
  156. package/deploy/systemd/opencode.service +25 -0
  157. package/docs/legacy-config-ui-reference.md +51 -0
  158. package/docs/memory-e2e-trace-2026-03-04.md +63 -0
  159. package/docs/memory-ops.md +96 -0
  160. package/docs/memory-runtime-contract.md +42 -0
  161. package/docs/memory-tuning-remote-2026-03-04.md +59 -0
  162. package/docs/opencode-rebase-workflow-plan.md +614 -0
  163. package/docs/opencode-startup-sync-plan.md +94 -0
  164. package/docs/vendor-opencode.md +41 -0
  165. package/drizzle/0000_famous_turbo.sql +49 -0
  166. package/drizzle/0001_cron_memory_aux.sql +160 -0
  167. package/drizzle/0002_runtime_session_bindings.sql +28 -0
  168. package/drizzle/0003_background_runs.sql +27 -0
  169. package/drizzle/0004_memory_open_write.sql +63 -0
  170. package/drizzle/0005_signal_channel.sql +47 -0
  171. package/drizzle/0006_usage_event_dimensions.sql +7 -0
  172. package/drizzle/meta/0000_snapshot.json +341 -0
  173. package/drizzle/meta/_journal.json +55 -0
  174. package/drizzle.config.ts +14 -0
  175. package/eslint.config.mjs +77 -0
  176. package/knip.json +18 -0
  177. package/memory/2026-03-04.md +4 -0
  178. package/opencode.lock.json +16 -0
  179. package/package.json +67 -0
  180. package/packages/agent-mockingbird-installer/README.md +31 -0
  181. package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
  182. package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
  183. package/packages/agent-mockingbird-installer/package.json +23 -0
  184. package/packages/contracts/package.json +19 -0
  185. package/packages/contracts/src/agentTypes.ts +122 -0
  186. package/packages/contracts/src/cron.ts +146 -0
  187. package/packages/contracts/src/dashboard.ts +378 -0
  188. package/packages/contracts/src/index.ts +3 -0
  189. package/packages/contracts/tsconfig.json +4 -0
  190. package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
  191. package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
  192. package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
  193. package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
  194. package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
  195. package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
  196. package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
  197. package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
  198. package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
  199. package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
  200. package/runtime-assets/opencode-config/opencode.jsonc +25 -0
  201. package/runtime-assets/opencode-config/package.json +5 -0
  202. package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
  203. package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
  204. package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
  205. package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
  206. package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
  207. package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
  208. package/runtime-assets/workspace/AGENTS.md +56 -0
  209. package/runtime-assets/workspace/MEMORY.md +4 -0
  210. package/scripts/build-release-bundle.sh +66 -0
  211. package/scripts/check-ship.ts +383 -0
  212. package/scripts/dev-opencode.sh +17 -0
  213. package/scripts/dev-stack-opencode.sh +15 -0
  214. package/scripts/dev-stack.sh +61 -0
  215. package/scripts/install-systemd.sh +87 -0
  216. package/scripts/memory-e2e.sh +76 -0
  217. package/scripts/memory-trace-e2e.sh +141 -0
  218. package/scripts/migrate-opencode-env.ts +108 -0
  219. package/scripts/onboard/bootstrap.sh +32 -0
  220. package/scripts/opencode-swap.ts +78 -0
  221. package/scripts/opencode-sync.ts +715 -0
  222. package/scripts/runtime-assets-sync.mjs +83 -0
  223. package/scripts/setup-git-hooks.ts +39 -0
  224. package/tsconfig.json +45 -0
  225. package/tui.json +98 -0
  226. package/turbo.json +36 -0
  227. package/vendor/OPENCODE_VENDOR.md +13 -0
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { blendRrfAndRerank, hasStrongBm25Signal, reciprocalRankFusion } from "./qmdPort";
4
+
5
+ describe("hasStrongBm25Signal", () => {
6
+ test("returns true when score and gap thresholds are met", () => {
7
+ const ranked = [
8
+ { id: "a", score: 0.91 },
9
+ { id: "b", score: 0.72 },
10
+ ];
11
+ expect(hasStrongBm25Signal(ranked, { minScore: 0.85, minGap: 0.15 })).toBe(true);
12
+ });
13
+
14
+ test("returns false when top gap is too small", () => {
15
+ const ranked = [
16
+ { id: "a", score: 0.91 },
17
+ { id: "b", score: 0.8 },
18
+ ];
19
+ expect(hasStrongBm25Signal(ranked, { minScore: 0.85, minGap: 0.15 })).toBe(false);
20
+ });
21
+ });
22
+
23
+ describe("reciprocalRankFusion", () => {
24
+ test("boosts items that rank well across multiple lists", () => {
25
+ const results = reciprocalRankFusion(
26
+ [
27
+ [
28
+ { id: "x", score: 1 },
29
+ { id: "y", score: 0.9 },
30
+ ],
31
+ [
32
+ { id: "y", score: 1 },
33
+ { id: "x", score: 0.8 },
34
+ ],
35
+ ],
36
+ [1, 1],
37
+ 60,
38
+ );
39
+
40
+ expect(results.length).toBe(2);
41
+ expect(results[0]?.id).toBe("x");
42
+ expect(results[1]?.id).toBe("y");
43
+ });
44
+ });
45
+
46
+ describe("blendRrfAndRerank", () => {
47
+ test("protects top-ranked items with stronger rrf weight", () => {
48
+ const top = blendRrfAndRerank(1, 0.1);
49
+ const mid = blendRrfAndRerank(8, 0.1);
50
+ const tail = blendRrfAndRerank(20, 0.1);
51
+ expect(top).toBeGreaterThan(mid);
52
+ expect(mid).toBeGreaterThan(tail);
53
+ });
54
+ });
@@ -0,0 +1,61 @@
1
+ // Adapted from qmd (MIT): reciprocal-rank fusion and strong-signal gating.
2
+ // Source: https://github.com/tobi/qmd
3
+
4
+ interface RankedResult {
5
+ id: string;
6
+ score: number;
7
+ }
8
+
9
+ export interface ExpandedQuery {
10
+ type: "lex" | "vec" | "hyde";
11
+ text: string;
12
+ }
13
+
14
+ export function hasStrongBm25Signal(
15
+ ranked: RankedResult[],
16
+ thresholds: { minScore: number; minGap: number },
17
+ ): boolean {
18
+ if (!ranked.length) return false;
19
+ const topScore = ranked[0]?.score ?? 0;
20
+ const secondScore = ranked[1]?.score ?? 0;
21
+ return topScore >= thresholds.minScore && topScore - secondScore >= thresholds.minGap;
22
+ }
23
+
24
+ export function reciprocalRankFusion(resultLists: RankedResult[][], weights: number[] = [], k = 60): RankedResult[] {
25
+ const scores = new Map<string, { score: number; topRank: number }>();
26
+
27
+ for (let listIdx = 0; listIdx < resultLists.length; listIdx += 1) {
28
+ const list = resultLists[listIdx];
29
+ if (!list) continue;
30
+ const weight = weights[listIdx] ?? 1;
31
+ for (let rank = 0; rank < list.length; rank += 1) {
32
+ const result = list[rank];
33
+ if (!result) continue;
34
+ const contribution = weight / (k + rank + 1);
35
+ const existing = scores.get(result.id);
36
+ if (existing) {
37
+ existing.score += contribution;
38
+ existing.topRank = Math.min(existing.topRank, rank);
39
+ continue;
40
+ }
41
+ scores.set(result.id, { score: contribution, topRank: rank });
42
+ }
43
+ }
44
+
45
+ for (const entry of scores.values()) {
46
+ if (entry.topRank === 0) entry.score += 0.05;
47
+ else if (entry.topRank <= 2) entry.score += 0.02;
48
+ }
49
+
50
+ return [...scores.entries()]
51
+ .map(([id, value]) => ({ id, score: value.score }))
52
+ .sort((a, b) => b.score - a.score);
53
+ }
54
+
55
+ export function blendRrfAndRerank(rrfRank: number, rerankScore: number) {
56
+ let rrfWeight = 0.4;
57
+ if (rrfRank <= 3) rrfWeight = 0.75;
58
+ else if (rrfRank <= 10) rrfWeight = 0.6;
59
+ const rrfScore = 1 / Math.max(1, rrfRank);
60
+ return rrfWeight * rrfScore + (1 - rrfWeight) * rerankScore;
61
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { migrateLegacyMemoryMarkdownToV2, parseMemoryRecordBlocks, parseMemoryRecords } from "./records";
4
+
5
+ describe("parseMemoryRecordBlocks", () => {
6
+ test("extracts structured memory record blocks with line ranges", () => {
7
+ const content = [
8
+ "# Daily Notes",
9
+ "",
10
+ "### [memory:memory_a1] 2026-03-02T20:38:29.490Z",
11
+ "meta: source=user",
12
+ "",
13
+ "User is currently in the NICU with Lucy Lee.",
14
+ "",
15
+ "### [memory:memory_b2] 2026-03-02T20:41:01.037Z",
16
+ "meta: source=user; entities=Vulpix",
17
+ "",
18
+ "User's favorite Pokemon is Vulpix.",
19
+ "",
20
+ ].join("\n");
21
+
22
+ const blocks = parseMemoryRecordBlocks(content);
23
+ expect(blocks.length).toBe(2);
24
+ expect(blocks[0]?.recordId).toBe("memory_a1");
25
+ expect(blocks[0]?.startLine).toBe(3);
26
+ expect(blocks[0]?.endLine).toBe(6);
27
+ expect(blocks[1]?.recordId).toBe("memory_b2");
28
+ expect(blocks[1]?.startLine).toBe(8);
29
+ expect(blocks[1]?.endLine).toBe(11);
30
+ });
31
+ });
32
+
33
+ describe("parseMemoryRecords", () => {
34
+ test("continues parsing record metadata/body normally", () => {
35
+ const content = [
36
+ "### [memory:memory_z9] 2026-03-02T20:41:33.914Z",
37
+ "meta: source=user; entities=Android; confidence=1",
38
+ "",
39
+ "User has an Android phone.",
40
+ ].join("\n");
41
+ const records = parseMemoryRecords(content);
42
+ expect(records.length).toBe(1);
43
+ expect(records[0]?.id).toBe("memory_z9");
44
+ expect(records[0]?.content).toBe("User has an Android phone.");
45
+ expect(records[0]?.source).toBe("user");
46
+ expect(records[0]?.entities).toEqual(["Android"]);
47
+ });
48
+ });
49
+
50
+ describe("migrateLegacyMemoryMarkdownToV2", () => {
51
+ test("rewrites legacy JSON records into compact v2 format", () => {
52
+ const content = [
53
+ "### [memory:memory_z9] 2026-03-02T20:41:33.914Z",
54
+ "```json",
55
+ '{"id":"memory_z9","source":"user","recordedAt":"2026-03-02T20:41:33.914Z","entities":["Android"],"confidence":1}',
56
+ "```",
57
+ "User has an Android phone.",
58
+ "",
59
+ ].join("\n");
60
+
61
+ const migrated = migrateLegacyMemoryMarkdownToV2(content);
62
+ expect(migrated.migrated).toBe(1);
63
+ expect(migrated.content).toContain("meta: source=user; entities=Android");
64
+ expect(migrated.content).not.toContain("```json");
65
+ });
66
+ });
@@ -0,0 +1,229 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import type { MemoryRecord, MemoryRecordInput } from "./types";
4
+
5
+ const RECORD_PREFIX = "memory";
6
+ const RECORD_HEADING_RE = /^###\s+\[memory:([a-zA-Z0-9_-]+)\].*$/m;
7
+ const RECORD_BLOCK_RE =
8
+ /###\s+\[memory:([a-zA-Z0-9_-]+)\]\s+([^\n]+)\n(?:meta:\s*([^\n]*)\n)?(?:\n)?([\s\S]*?)(?=\n###\s+\[memory:|$)/g;
9
+ const LEGACY_RECORD_BLOCK_RE =
10
+ /###\s+\[memory:([a-zA-Z0-9_-]+)\][^\n]*\n```json\n([\s\S]*?)\n```\n([\s\S]*?)(?=\n###\s+\[memory:|$)/g;
11
+
12
+ interface ParsedMemoryRecordBlock {
13
+ recordId: string;
14
+ startLine: number;
15
+ endLine: number;
16
+ text: string;
17
+ }
18
+
19
+ function normalizeList(values: string[] | undefined): string[] {
20
+ if (!values?.length) {
21
+ return [];
22
+ }
23
+ return [...new Set(values.map(value => value.trim()).filter(Boolean))];
24
+ }
25
+
26
+ function clampConfidence(value: number | undefined): number {
27
+ if (typeof value !== "number" || Number.isNaN(value)) {
28
+ return 0.75;
29
+ }
30
+ return Math.max(0, Math.min(1, value));
31
+ }
32
+
33
+ function normalizeContent(content: string): string {
34
+ return content.trim().replace(/\r\n/g, "\n");
35
+ }
36
+
37
+ function parseMetaLine(metaLine: string | undefined): Partial<MemoryRecordInput> {
38
+ if (!metaLine) return {};
39
+ const parsed: Partial<MemoryRecordInput> = {};
40
+ const entries = metaLine
41
+ .split(";")
42
+ .map(item => item.trim())
43
+ .filter(Boolean);
44
+
45
+ for (const entry of entries) {
46
+ const delimiter = entry.indexOf("=");
47
+ if (delimiter <= 0) continue;
48
+ const key = entry.slice(0, delimiter).trim().toLowerCase();
49
+ const value = entry.slice(delimiter + 1).trim();
50
+ if (!value) continue;
51
+ if (key === "source") {
52
+ if (value === "user" || value === "assistant" || value === "system") {
53
+ parsed.source = value;
54
+ }
55
+ continue;
56
+ }
57
+ if (key === "confidence") {
58
+ const numeric = Number(value);
59
+ if (Number.isFinite(numeric)) {
60
+ parsed.confidence = clampConfidence(numeric);
61
+ }
62
+ continue;
63
+ }
64
+ if (key === "entities") {
65
+ parsed.entities = normalizeList(value.split(","));
66
+ continue;
67
+ }
68
+ if (key === "supersedes") {
69
+ parsed.supersedes = normalizeList(value.split(","));
70
+ }
71
+ }
72
+ return parsed;
73
+ }
74
+
75
+ function buildMetaLine(record: MemoryRecord): string {
76
+ const parts = [`source=${record.source}`];
77
+ const confidence = clampConfidence(record.confidence);
78
+ const entities = normalizeList(record.entities);
79
+ const supersedes = normalizeList(record.supersedes);
80
+ if (confidence !== 1) {
81
+ parts.push(`confidence=${Number(confidence.toFixed(4))}`);
82
+ }
83
+ if (entities.length > 0) {
84
+ parts.push(`entities=${entities.join(", ")}`);
85
+ }
86
+ if (supersedes.length > 0) {
87
+ parts.push(`supersedes=${supersedes.join(",")}`);
88
+ }
89
+ return `meta: ${parts.join("; ")}`;
90
+ }
91
+
92
+ export function createMemoryRecord(input: MemoryRecordInput): MemoryRecord {
93
+ const createdAt = new Date().toISOString();
94
+ const id = `${RECORD_PREFIX}_${crypto.randomUUID().replaceAll("-", "").slice(0, 12)}`;
95
+ return {
96
+ id,
97
+ recordedAt: createdAt,
98
+ source: input.source,
99
+ content: normalizeContent(input.content),
100
+ entities: normalizeList(input.entities),
101
+ confidence: clampConfidence(input.confidence),
102
+ supersedes: normalizeList(input.supersedes),
103
+ };
104
+ }
105
+
106
+ export function formatMemoryRecord(record: MemoryRecord): string {
107
+ return [
108
+ `### [memory:${record.id}] ${record.recordedAt}`,
109
+ buildMetaLine(record),
110
+ "",
111
+ record.content,
112
+ "",
113
+ ].join("\n");
114
+ }
115
+
116
+ export function parseMemoryRecords(content: string): MemoryRecord[] {
117
+ const records: MemoryRecord[] = [];
118
+ const normalized = content.replace(/\r\n/g, "\n");
119
+
120
+ let match: RegExpExecArray | null = RECORD_BLOCK_RE.exec(normalized);
121
+ while (match) {
122
+ const parsedId = match[1]?.trim();
123
+ const recordedAt = match[2]?.trim();
124
+ const metaRaw = match[3]?.trim();
125
+ const bodyRaw = match[4]?.trim() ?? "";
126
+ if (!parsedId || !recordedAt) {
127
+ match = RECORD_BLOCK_RE.exec(normalized);
128
+ continue;
129
+ }
130
+
131
+ const metadata = parseMetaLine(metaRaw);
132
+ records.push({
133
+ id: parsedId,
134
+ source: metadata.source ?? "system",
135
+ recordedAt,
136
+ content: bodyRaw,
137
+ entities: normalizeList(metadata.entities),
138
+ supersedes: normalizeList(metadata.supersedes),
139
+ confidence: clampConfidence(metadata.confidence),
140
+ });
141
+
142
+ match = RECORD_BLOCK_RE.exec(normalized);
143
+ }
144
+
145
+ return records;
146
+ }
147
+
148
+ export function parseMemoryRecordBlocks(content: string): ParsedMemoryRecordBlock[] {
149
+ const blocks: ParsedMemoryRecordBlock[] = [];
150
+ const normalized = content.replace(/\r\n/g, "\n");
151
+ let match: RegExpExecArray | null = RECORD_BLOCK_RE.exec(normalized);
152
+ while (match) {
153
+ const full = match[0] ?? "";
154
+ const recordId = match[1]?.trim();
155
+ const startOffset = match.index;
156
+ const endOffsetExclusive = startOffset + full.length;
157
+ if (!recordId || startOffset < 0 || !full.trim()) {
158
+ match = RECORD_BLOCK_RE.exec(normalized);
159
+ continue;
160
+ }
161
+ const startLine = normalized.slice(0, startOffset).split("\n").length;
162
+ const endLine = normalized.slice(0, Math.max(startOffset, endOffsetExclusive - 1)).split("\n").length;
163
+ blocks.push({
164
+ recordId,
165
+ startLine,
166
+ endLine,
167
+ text: full.trimEnd(),
168
+ });
169
+ match = RECORD_BLOCK_RE.exec(normalized);
170
+ }
171
+ return blocks;
172
+ }
173
+
174
+ export function extractRecordIdFromChunk(text: string): string | null {
175
+ const headingMatch = text.match(RECORD_HEADING_RE);
176
+ const rawId = headingMatch?.[1]?.trim();
177
+ if (!rawId) {
178
+ return null;
179
+ }
180
+ return rawId;
181
+ }
182
+
183
+ export function migrateLegacyMemoryMarkdownToV2(content: string): { content: string; migrated: number } {
184
+ const normalized = content.replace(/\r\n/g, "\n");
185
+ const migrated: string[] = [];
186
+ let migratedCount = 0;
187
+ let match: RegExpExecArray | null = LEGACY_RECORD_BLOCK_RE.exec(normalized);
188
+ while (match) {
189
+ const parsedId = match[1]?.trim();
190
+ const metaRaw = match[2]?.trim();
191
+ const bodyRaw = match[3]?.trim() ?? "";
192
+ if (!parsedId || !metaRaw) {
193
+ match = LEGACY_RECORD_BLOCK_RE.exec(normalized);
194
+ continue;
195
+ }
196
+
197
+ try {
198
+ const metadata = JSON.parse(metaRaw) as Partial<MemoryRecord>;
199
+ const source = metadata.source ?? "system";
200
+ const recordedAt =
201
+ typeof metadata.recordedAt === "string" && metadata.recordedAt.trim()
202
+ ? metadata.recordedAt.trim()
203
+ : new Date().toISOString();
204
+ const record: MemoryRecord = {
205
+ id: parsedId,
206
+ source: source === "user" || source === "assistant" || source === "system" ? source : "system",
207
+ recordedAt,
208
+ content: bodyRaw,
209
+ entities: normalizeList(metadata.entities),
210
+ supersedes: normalizeList(metadata.supersedes),
211
+ confidence: clampConfidence(metadata.confidence),
212
+ };
213
+ migrated.push(formatMemoryRecord(record).trimEnd());
214
+ migratedCount += 1;
215
+ } catch {
216
+ // ignore malformed legacy records
217
+ }
218
+
219
+ match = LEGACY_RECORD_BLOCK_RE.exec(normalized);
220
+ }
221
+
222
+ if (migratedCount === 0) {
223
+ return { content: normalized, migrated: 0 };
224
+ }
225
+ return {
226
+ content: `${migrated.join("\n\n")}\n`,
227
+ migrated: migratedCount,
228
+ };
229
+ }