@geminixiang/mikan 1.0.0-beta.87 → 1.0.0-beta.89

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 (249) hide show
  1. package/CHANGELOG.md +44 -1370
  2. package/README.md +2 -2
  3. package/dist/adapters/commands/session-view.d.ts.map +1 -1
  4. package/dist/adapters/commands/session-view.js +4 -4
  5. package/dist/adapters/commands/session-view.js.map +1 -1
  6. package/dist/adapters/discord/bot.d.ts +4 -1
  7. package/dist/adapters/discord/bot.d.ts.map +1 -1
  8. package/dist/adapters/discord/bot.js +6 -3
  9. package/dist/adapters/discord/bot.js.map +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +3 -1
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/discord/types.d.ts +1 -0
  14. package/dist/adapters/discord/types.d.ts.map +1 -1
  15. package/dist/adapters/discord/types.js.map +1 -1
  16. package/dist/adapters/github/activity.d.ts.map +1 -1
  17. package/dist/adapters/github/activity.js +7 -13
  18. package/dist/adapters/github/activity.js.map +1 -1
  19. package/dist/adapters/github/bot.d.ts +4 -1
  20. package/dist/adapters/github/bot.d.ts.map +1 -1
  21. package/dist/adapters/github/bot.js +3 -3
  22. package/dist/adapters/github/bot.js.map +1 -1
  23. package/dist/adapters/github/context.d.ts.map +1 -1
  24. package/dist/adapters/github/context.js +3 -1
  25. package/dist/adapters/github/context.js.map +1 -1
  26. package/dist/adapters/github/types.d.ts +0 -3
  27. package/dist/adapters/github/types.d.ts.map +1 -1
  28. package/dist/adapters/github/types.js.map +1 -1
  29. package/dist/adapters/intake.d.ts.map +1 -1
  30. package/dist/adapters/intake.js +12 -2
  31. package/dist/adapters/intake.js.map +1 -1
  32. package/dist/adapters/shared.d.ts +2 -1
  33. package/dist/adapters/shared.d.ts.map +1 -1
  34. package/dist/adapters/shared.js +7 -2
  35. package/dist/adapters/shared.js.map +1 -1
  36. package/dist/adapters/slack/assistant.d.ts +2 -4
  37. package/dist/adapters/slack/assistant.d.ts.map +1 -1
  38. package/dist/adapters/slack/assistant.js +10 -16
  39. package/dist/adapters/slack/assistant.js.map +1 -1
  40. package/dist/adapters/slack/bot.d.ts +5 -1
  41. package/dist/adapters/slack/bot.d.ts.map +1 -1
  42. package/dist/adapters/slack/bot.js +15 -11
  43. package/dist/adapters/slack/bot.js.map +1 -1
  44. package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
  45. package/dist/adapters/slack/response-lifecycle.js +1 -1
  46. package/dist/adapters/slack/response-lifecycle.js.map +1 -1
  47. package/dist/adapters/slack/task-status.d.ts.map +1 -1
  48. package/dist/adapters/slack/task-status.js +1 -2
  49. package/dist/adapters/slack/task-status.js.map +1 -1
  50. package/dist/adapters/telegram/bot.d.ts +4 -1
  51. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  52. package/dist/adapters/telegram/bot.js +3 -3
  53. package/dist/adapters/telegram/bot.js.map +1 -1
  54. package/dist/adapters/telegram/context.d.ts.map +1 -1
  55. package/dist/adapters/telegram/context.js +3 -1
  56. package/dist/adapters/telegram/context.js.map +1 -1
  57. package/dist/adapters/types.d.ts +3 -1
  58. package/dist/adapters/types.d.ts.map +1 -1
  59. package/dist/adapters/types.js.map +1 -1
  60. package/dist/adapters/web/admin/client-assets.js +1 -1
  61. package/dist/adapters/web/admin/client-assets.js.map +1 -1
  62. package/dist/adapters/web/admin/portal.d.ts +0 -1
  63. package/dist/adapters/web/admin/portal.d.ts.map +1 -1
  64. package/dist/adapters/web/admin/portal.js +26 -42
  65. package/dist/adapters/web/admin/portal.js.map +1 -1
  66. package/dist/adapters/web/admin/types.d.ts +0 -2
  67. package/dist/adapters/web/admin/types.d.ts.map +1 -1
  68. package/dist/adapters/web/admin/types.js.map +1 -1
  69. package/dist/adapters/web/login/portal.d.ts.map +1 -1
  70. package/dist/adapters/web/login/portal.js +0 -8
  71. package/dist/adapters/web/login/portal.js.map +1 -1
  72. package/dist/adapters/web/server.d.ts.map +1 -1
  73. package/dist/adapters/web/server.js +0 -1
  74. package/dist/adapters/web/server.js.map +1 -1
  75. package/dist/adapters/web/session-view/portal.d.ts +1 -2
  76. package/dist/adapters/web/session-view/portal.d.ts.map +1 -1
  77. package/dist/adapters/web/session-view/portal.js +38 -328
  78. package/dist/adapters/web/session-view/portal.js.map +1 -1
  79. package/dist/adapters/web/session-view/types.d.ts +4 -6
  80. package/dist/adapters/web/session-view/types.d.ts.map +1 -1
  81. package/dist/adapters/web/session-view/types.js.map +1 -1
  82. package/dist/env-manifest.d.ts +0 -4
  83. package/dist/env-manifest.d.ts.map +1 -1
  84. package/dist/env-manifest.js +5 -8
  85. package/dist/env-manifest.js.map +1 -1
  86. package/dist/events/index.d.ts +2 -9
  87. package/dist/events/index.d.ts.map +1 -1
  88. package/dist/events/index.js +0 -1
  89. package/dist/events/index.js.map +1 -1
  90. package/dist/events/scheduler.d.ts +0 -1
  91. package/dist/events/scheduler.d.ts.map +1 -1
  92. package/dist/events/scheduler.js +2 -7
  93. package/dist/events/scheduler.js.map +1 -1
  94. package/dist/harness/execution-env.d.ts +1 -1
  95. package/dist/harness/execution-env.d.ts.map +1 -1
  96. package/dist/harness/execution-env.js +32 -32
  97. package/dist/harness/execution-env.js.map +1 -1
  98. package/dist/harness/index.d.ts +2 -3
  99. package/dist/harness/index.d.ts.map +1 -1
  100. package/dist/harness/index.js +0 -1
  101. package/dist/harness/index.js.map +1 -1
  102. package/dist/harness/mcp-result.d.ts +4 -3
  103. package/dist/harness/mcp-result.d.ts.map +1 -1
  104. package/dist/harness/mcp-result.js +27 -13
  105. package/dist/harness/mcp-result.js.map +1 -1
  106. package/dist/harness/mcp.js +4 -4
  107. package/dist/harness/mcp.js.map +1 -1
  108. package/dist/harness/presenter.d.ts.map +1 -1
  109. package/dist/harness/presenter.js +20 -25
  110. package/dist/harness/presenter.js.map +1 -1
  111. package/dist/harness/prompt.d.ts +0 -1
  112. package/dist/harness/prompt.d.ts.map +1 -1
  113. package/dist/harness/prompt.js +3 -32
  114. package/dist/harness/prompt.js.map +1 -1
  115. package/dist/harness/run-events.d.ts.map +1 -1
  116. package/dist/harness/run-events.js +4 -12
  117. package/dist/harness/run-events.js.map +1 -1
  118. package/dist/harness/runner.d.ts.map +1 -1
  119. package/dist/harness/runner.js +35 -38
  120. package/dist/harness/runner.js.map +1 -1
  121. package/dist/harness/session.d.ts +37 -38
  122. package/dist/harness/session.d.ts.map +1 -1
  123. package/dist/harness/session.js +387 -388
  124. package/dist/harness/session.js.map +1 -1
  125. package/dist/harness/subagent.d.ts +5 -5
  126. package/dist/harness/subagent.d.ts.map +1 -1
  127. package/dist/harness/subagent.js +40 -45
  128. package/dist/harness/subagent.js.map +1 -1
  129. package/dist/harness/tools/codemode.d.ts +3 -0
  130. package/dist/harness/tools/codemode.d.ts.map +1 -0
  131. package/dist/harness/tools/codemode.js +173 -0
  132. package/dist/harness/tools/codemode.js.map +1 -0
  133. package/dist/harness/tools/history.d.ts +20 -0
  134. package/dist/harness/tools/history.d.ts.map +1 -0
  135. package/dist/harness/tools/history.js +293 -0
  136. package/dist/harness/tools/history.js.map +1 -0
  137. package/dist/harness/tools/pi-tools.d.ts +1 -1
  138. package/dist/harness/tools/pi-tools.d.ts.map +1 -1
  139. package/dist/harness/tools/pi-tools.js +47 -21
  140. package/dist/harness/tools/pi-tools.js.map +1 -1
  141. package/dist/harness/tools/secret-redaction.d.ts.map +1 -1
  142. package/dist/harness/tools/secret-redaction.js +46 -2
  143. package/dist/harness/tools/secret-redaction.js.map +1 -1
  144. package/dist/harness/tools/tool-search.d.ts +5 -0
  145. package/dist/harness/tools/tool-search.d.ts.map +1 -0
  146. package/dist/harness/tools/tool-search.js +84 -0
  147. package/dist/harness/tools/tool-search.js.map +1 -0
  148. package/dist/harness/types.d.ts +75 -15
  149. package/dist/harness/types.d.ts.map +1 -1
  150. package/dist/harness/types.js.map +1 -1
  151. package/dist/index.d.ts +3 -7
  152. package/dist/index.d.ts.map +1 -1
  153. package/dist/index.js +0 -3
  154. package/dist/index.js.map +1 -1
  155. package/dist/log.d.ts +0 -3
  156. package/dist/log.d.ts.map +1 -1
  157. package/dist/log.js +0 -15
  158. package/dist/log.js.map +1 -1
  159. package/dist/main.js +2 -2
  160. package/dist/main.js.map +1 -1
  161. package/dist/memory-capture/index.js +0 -1
  162. package/dist/memory-capture/index.js.map +1 -1
  163. package/dist/migrations/index.d.ts.map +1 -1
  164. package/dist/migrations/index.js +2 -0
  165. package/dist/migrations/index.js.map +1 -1
  166. package/dist/migrations/session-files.d.ts +3 -2
  167. package/dist/migrations/session-files.d.ts.map +1 -1
  168. package/dist/migrations/session-files.js.map +1 -1
  169. package/dist/migrations/session-v4.d.ts +77 -0
  170. package/dist/migrations/session-v4.d.ts.map +1 -0
  171. package/dist/migrations/session-v4.js +177 -0
  172. package/dist/migrations/session-v4.js.map +1 -0
  173. package/dist/migrations/sessions-sqlite.d.ts +3 -0
  174. package/dist/migrations/sessions-sqlite.d.ts.map +1 -0
  175. package/dist/migrations/sessions-sqlite.js +215 -0
  176. package/dist/migrations/sessions-sqlite.js.map +1 -0
  177. package/dist/migrations/sessions-v3.d.ts +2 -2
  178. package/dist/migrations/sessions-v3.d.ts.map +1 -1
  179. package/dist/migrations/sessions-v3.js +8 -19
  180. package/dist/migrations/sessions-v3.js.map +1 -1
  181. package/dist/migrations/types.d.ts +0 -5
  182. package/dist/migrations/types.d.ts.map +1 -1
  183. package/dist/migrations/types.js.map +1 -1
  184. package/dist/observability/sentry.d.ts +2 -9
  185. package/dist/observability/sentry.d.ts.map +1 -1
  186. package/dist/observability/sentry.js +12 -64
  187. package/dist/observability/sentry.js.map +1 -1
  188. package/dist/observability/types.d.ts +0 -16
  189. package/dist/observability/types.d.ts.map +1 -1
  190. package/dist/observability/types.js.map +1 -1
  191. package/dist/office/index.d.ts +1 -4
  192. package/dist/office/index.d.ts.map +1 -1
  193. package/dist/office/index.js +2 -14
  194. package/dist/office/index.js.map +1 -1
  195. package/dist/office/projection.d.ts.map +1 -1
  196. package/dist/office/projection.js +0 -1
  197. package/dist/office/projection.js.map +1 -1
  198. package/dist/office/types.d.ts +1 -2
  199. package/dist/office/types.d.ts.map +1 -1
  200. package/dist/office/types.js.map +1 -1
  201. package/dist/runtime/conversation-runtime.d.ts.map +1 -1
  202. package/dist/runtime/conversation-runtime.js +2 -16
  203. package/dist/runtime/conversation-runtime.js.map +1 -1
  204. package/dist/runtime/session-lifecycle.d.ts +1 -2
  205. package/dist/runtime/session-lifecycle.d.ts.map +1 -1
  206. package/dist/runtime/session-lifecycle.js +7 -16
  207. package/dist/runtime/session-lifecycle.js.map +1 -1
  208. package/dist/runtime/types.d.ts +0 -1
  209. package/dist/runtime/types.d.ts.map +1 -1
  210. package/dist/runtime/types.js.map +1 -1
  211. package/dist/sandbox/provisioner.d.ts.map +1 -1
  212. package/dist/sandbox/provisioner.js +3 -4
  213. package/dist/sandbox/provisioner.js.map +1 -1
  214. package/dist/sessions/chat-history-sync.d.ts +8 -6
  215. package/dist/sessions/chat-history-sync.d.ts.map +1 -1
  216. package/dist/sessions/chat-history-sync.js +70 -143
  217. package/dist/sessions/chat-history-sync.js.map +1 -1
  218. package/dist/sessions/compaction-summary.d.ts +4 -0
  219. package/dist/sessions/compaction-summary.d.ts.map +1 -0
  220. package/dist/sessions/compaction-summary.js +16 -0
  221. package/dist/sessions/compaction-summary.js.map +1 -0
  222. package/dist/sessions/session-key.d.ts +1 -0
  223. package/dist/sessions/session-key.d.ts.map +1 -1
  224. package/dist/sessions/session-key.js +3 -0
  225. package/dist/sessions/session-key.js.map +1 -1
  226. package/dist/sessions/session-store.d.ts +47 -29
  227. package/dist/sessions/session-store.d.ts.map +1 -1
  228. package/dist/sessions/session-store.js +578 -484
  229. package/dist/sessions/session-store.js.map +1 -1
  230. package/dist/sessions/types.d.ts +60 -46
  231. package/dist/sessions/types.d.ts.map +1 -1
  232. package/dist/sessions/types.js +1 -1
  233. package/dist/sessions/types.js.map +1 -1
  234. package/dist/settings/apply.js +2 -2
  235. package/dist/settings/apply.js.map +1 -1
  236. package/dist/types.d.ts +10 -3
  237. package/dist/types.d.ts.map +1 -1
  238. package/dist/types.js.map +1 -1
  239. package/dist/vault/index.d.ts.map +1 -1
  240. package/dist/vault/index.js +0 -3
  241. package/dist/vault/index.js.map +1 -1
  242. package/dist/vault/types.d.ts +0 -3
  243. package/dist/vault/types.d.ts.map +1 -1
  244. package/dist/vault/types.js.map +1 -1
  245. package/package.json +8 -5
  246. package/dist/sessions/store.d.ts +0 -17
  247. package/dist/sessions/store.d.ts.map +0 -1
  248. package/dist/sessions/store.js +0 -211
  249. package/dist/sessions/store.js.map +0 -1
@@ -1,303 +1,421 @@
1
- import { closeSync, existsSync, fstatSync, ftruncateSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { basename, dirname, join, resolve } from "node:path";
4
- import { AgentHarness, laneState, operationResult, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, JsonlSessionRepo, JSONL_STORAGE_VERSION, MemorySessionRepo, TODO_CONTEXT, uuidv7, value, } from "@earendil-works/pi-agent-core";
5
- import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
6
- import { atomicWritePrivateFile } from "../file-guards.js";
7
- import { CURRENT_SESSION_VERSION } from "./types.js";
1
+ import { chmodSync, existsSync, mkdirSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname } from "node:path";
4
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
5
+ import { calculateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
6
+ import { AssistantEntry, CompactionEntry, Harness, LiveDoc, MemoryStorage, ROOT_CONVERSATION_ID, ToolResultEntry, UserEntry, createRegistry, defineDoc, defineEntry, } from "@earendil-works/pi-durable";
7
+ import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
8
+ import { RUN_CAUSE_CUSTOM_TYPE } from "./types.js";
8
9
  import { loadMcpTools, formatMcpServerInstructions } from "../harness/mcp.js";
9
10
  import * as log from "../log.js";
10
- import { isRecord } from "../unknown-values.js";
11
- const MIKAN_METADATA = value("mikan", "metadata");
12
- const activeWriterKeys = new Map();
13
- function canonicalSessionPath(path) {
14
- const absolute = resolve(path);
15
- if (existsSync(absolute))
16
- return realpathSync(absolute);
17
- const parent = dirname(absolute);
18
- mkdirSync(parent, { recursive: true });
19
- return join(realpathSync(parent), basename(absolute));
11
+ import { compactionSummaryOf, wrapCompactionSummary } from "./compaction-summary.js";
12
+ import { isThreadSessionKey } from "./session-key.js";
13
+ import { errorMessage, isRecord } from "../unknown-values.js";
14
+ const context = BACKGROUND_CONTEXT;
15
+ const ENTRY_PAGE_SIZE = 500;
16
+ const SessionDoc = defineDoc({
17
+ kind: "mikan.session",
18
+ version: 1,
19
+ scope: "conversation",
20
+ history: "latest",
21
+ fork: "initial",
22
+ initial: () => ({}),
23
+ });
24
+ const SessionIndexDoc = defineDoc({
25
+ kind: "mikan.sessions",
26
+ version: 1,
27
+ scope: "session",
28
+ initial: () => ({ sessions: {} }),
29
+ });
30
+ const CustomSessionEntry = defineEntry("mikan.custom");
31
+ function toJson(input) {
32
+ if (input === undefined)
33
+ return null;
34
+ const copy = JSON.parse(JSON.stringify(input));
35
+ return copy;
20
36
  }
21
- function sessionWriterKey(path) {
22
- if (!existsSync(path))
23
- return `path:${path}`;
24
- const stats = statSync(path);
25
- return `inode:${stats.dev}:${stats.ino}:${stats.birthtimeMs}`;
37
+ function durableMessage(message) {
38
+ const copy = JSON.parse(JSON.stringify(message));
39
+ return copy;
26
40
  }
27
- function isStaleClaim(key, claimedPath) {
28
- if (!key.startsWith("inode:"))
29
- return false;
30
- return sessionWriterKey(claimedPath) !== key;
41
+ function messageTimestamp(message) {
42
+ return message && "timestamp" in message && typeof message.timestamp === "number"
43
+ ? message.timestamp
44
+ : 0;
45
+ }
46
+ function contentText(message) {
47
+ if (!message || !("content" in message))
48
+ return "";
49
+ if (typeof message.content === "string")
50
+ return message.content;
51
+ return message.content
52
+ .map((part) => (part.type === "text" ? part.text : ""))
53
+ .filter(Boolean)
54
+ .join("\n");
31
55
  }
32
- function claimWriter(key, path) {
33
- const claimedPath = activeWriterKeys.get(key);
34
- if (claimedPath !== undefined && !isStaleClaim(key, claimedPath)) {
35
- throw new Error(`Session file already has an active writer: ${path}`);
56
+ function toSessionEntry(record, sessionId) {
57
+ const id = `${sessionId}:${record.id}`;
58
+ const message = record.model?.[0];
59
+ if (UserEntry.is(record) || AssistantEntry.is(record) || ToolResultEntry.is(record)) {
60
+ if (!message)
61
+ return undefined;
62
+ return { type: "message", id, timestamp: messageTimestamp(message), message };
63
+ }
64
+ if (CompactionEntry.is(record)) {
65
+ return {
66
+ type: "compaction",
67
+ id,
68
+ timestamp: messageTimestamp(message),
69
+ summary: (message && compactionSummaryOf(message)) ?? contentText(message),
70
+ };
36
71
  }
37
- activeWriterKeys.set(key, path);
72
+ if (CustomSessionEntry.is(record)) {
73
+ return {
74
+ type: "custom",
75
+ id,
76
+ timestamp: record.data.timestamp,
77
+ customType: record.data.customType,
78
+ data: record.data.data ?? undefined,
79
+ };
80
+ }
81
+ return undefined;
38
82
  }
39
- function acquireWriter(path) {
40
- const canonical = canonicalSessionPath(path);
41
- const key = sessionWriterKey(canonical);
42
- claimWriter(key, canonical);
43
- return { path: canonical, key };
83
+ function toSessionEntries(records, sessionId) {
84
+ return records.flatMap((record) => toSessionEntry(record, sessionId) ?? []);
44
85
  }
45
- function releaseWriter(key) {
46
- activeWriterKeys.delete(key);
86
+ async function readRecords(conversation) {
87
+ const records = [];
88
+ let cursor;
89
+ do {
90
+ const page = await conversation.entries({}, ENTRY_PAGE_SIZE, cursor, context);
91
+ records.push(...page.items);
92
+ cursor = page.next;
93
+ } while (cursor !== undefined);
94
+ return records.toReversed();
47
95
  }
48
- function promotePendingWriter(pathKey, path) {
49
- const inodeKey = sessionWriterKey(path);
50
- if (inodeKey === pathKey)
51
- return pathKey;
52
- claimWriter(inodeKey, path);
53
- activeWriterKeys.delete(pathKey);
54
- return inodeKey;
96
+ async function readContext(conversation) {
97
+ const view = await conversation.context(context);
98
+ const messages = view.messages
99
+ .filter((message) => message.role !== "system")
100
+ .map(durableMessage);
101
+ return { messages };
55
102
  }
56
- function toDurable(input) {
57
- return input === undefined ? input : JSON.parse(JSON.stringify(input));
103
+ function bindModels(models, property) {
104
+ const value = Reflect.get(models, property, models);
105
+ return typeof value === "function" ? value.bind(models) : value;
58
106
  }
59
- class SessionFormatError extends Error {
107
+ function requestOwnerKey(request) {
108
+ if (typeof request !== "object" || request === null || !("messages" in request))
109
+ return undefined;
110
+ const { messages } = request;
111
+ if (!Array.isArray(messages))
112
+ return undefined;
113
+ const last = messages.at(-1);
114
+ return typeof last === "object" && last !== null ? last : undefined;
60
115
  }
61
- const HEADER_CHUNK_SIZE = 4096;
62
- const MAX_HEADER_BYTES = 1024 * 1024;
63
- function readHeaderLine(filePath) {
64
- const fd = openSync(filePath, "r");
65
- const chunks = [];
66
- let total = 0;
67
- try {
68
- while (total < MAX_HEADER_BYTES) {
69
- const chunk = Buffer.allocUnsafe(Math.min(HEADER_CHUNK_SIZE, MAX_HEADER_BYTES - total));
70
- const bytesRead = readSync(fd, chunk, 0, chunk.length, null);
71
- if (bytesRead === 0)
72
- break;
73
- const data = chunk.subarray(0, bytesRead);
74
- const newline = data.indexOf(0x0a);
75
- chunks.push(newline < 0 ? data : data.subarray(0, newline));
76
- total += newline < 0 ? bytesRead : newline;
77
- if (newline >= 0)
78
- break;
79
- }
116
+ function protectStorageFiles(path) {
117
+ for (const file of [path, `${path}-wal`, `${path}-shm`]) {
118
+ if (existsSync(file))
119
+ chmodSync(file, 0o600);
80
120
  }
81
- finally {
82
- closeSync(fd);
83
- }
84
- if (total >= MAX_HEADER_BYTES) {
85
- throw new SessionFormatError(`Session file header exceeds ${MAX_HEADER_BYTES} bytes: ${filePath}`);
86
- }
87
- return Buffer.concat(chunks).toString("utf-8");
88
121
  }
89
- function parseCurrentHeader(filePath, firstLine) {
90
- const trimmed = firstLine.trim();
91
- if (!trimmed)
92
- throw new SessionFormatError(`Session file has a blank header line: ${filePath}`);
93
- let parsed;
94
- try {
95
- parsed = JSON.parse(trimmed);
122
+ const REQUEST_METHODS = new Set(["streamSimple", "completeSimple"]);
123
+ class OfficeStorage {
124
+ static { this.pool = new Map(); }
125
+ constructor(path) {
126
+ this.path = path;
127
+ this.registry = createRegistry();
128
+ this.refs = 0;
129
+ this.bindings = new Map();
130
+ this.owners = new WeakMap();
131
+ this.writers = new Set();
132
+ }
133
+ get harness() {
134
+ if (!this.opened)
135
+ throw new Error("Office session storage is not open");
136
+ return this.opened;
137
+ }
138
+ static async acquire(path) {
139
+ let pending = OfficeStorage.pool.get(path);
140
+ if (!pending) {
141
+ pending = OfficeStorage.openShared(path);
142
+ OfficeStorage.pool.set(path, pending);
143
+ pending.catch(() => OfficeStorage.pool.delete(path));
144
+ }
145
+ const storage = await pending;
146
+ storage.refs += 1;
147
+ return storage;
148
+ }
149
+ static async inMemory() {
150
+ const storage = await OfficeStorage.open(null, new MemoryStorage());
151
+ storage.refs = 1;
152
+ return storage;
153
+ }
154
+ static async openShared(path) {
155
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
156
+ const storage = await OfficeStorage.open(path, await openNodeSqliteStorage(path));
157
+ protectStorageFiles(path);
158
+ await storage.abortUnfinishedWork();
159
+ return storage;
160
+ }
161
+ static async open(path, backend) {
162
+ const storage = new OfficeStorage(path);
163
+ storage.opened = await Harness.open(backend, storage.harnessOptions(), context);
164
+ return storage;
165
+ }
166
+ harnessOptions() {
167
+ const latest = () => [...this.bindings.values()].at(-1);
168
+ const fallback = () => {
169
+ const models = latest()?.models;
170
+ if (!models)
171
+ throw new Error("Session has no model binding; start a run to use the model");
172
+ return models;
173
+ };
174
+ const models = new Proxy({}, {
175
+ get: (_target, property) => {
176
+ if (!REQUEST_METHODS.has(property))
177
+ return bindModels(fallback(), property);
178
+ return (...args) => {
179
+ const key = requestOwnerKey(args[1]);
180
+ const owner = key ? this.owners.get(key) : undefined;
181
+ const binding = owner === undefined ? undefined : this.bindings.get(owner);
182
+ const target = binding?.requestModels ?? binding?.models ?? fallback();
183
+ const method = Reflect.get(target, property, target);
184
+ return method.apply(target, args);
185
+ };
186
+ },
187
+ });
188
+ const settings = () => latest()?.settings;
189
+ return {
190
+ models,
191
+ registry: this.registry,
192
+ settings: {
193
+ extensions: [],
194
+ get stream() {
195
+ return settings()?.stream;
196
+ },
197
+ get retry() {
198
+ return settings()?.retry;
199
+ },
200
+ get compaction() {
201
+ return settings()?.compaction;
202
+ },
203
+ get toolExecution() {
204
+ return settings()?.toolExecution;
205
+ },
206
+ get steeringMode() {
207
+ return settings()?.steeringMode;
208
+ },
209
+ get followUpMode() {
210
+ return settings()?.followUpMode;
211
+ },
212
+ },
213
+ env: (target, envContext) => this.bindings.get(target.conversationId)?.env?.(target, envContext),
214
+ onReport: (error) => {
215
+ const report = latest()?.onReport;
216
+ if (report)
217
+ report(error);
218
+ else
219
+ log.logWarning("Durable harness report", errorMessage(error));
220
+ },
221
+ };
96
222
  }
97
- catch {
98
- throw new SessionFormatError(`Session file header is not valid JSON: ${filePath}`);
223
+ async abortUnfinishedWork() {
224
+ const { tasks } = await this.harness.inspect(context);
225
+ const unowned = tasks.filter((task) => !task.record.owner);
226
+ if (unowned.length === 0)
227
+ return;
228
+ log.logWarning(`Aborting ${unowned.length} durable tasks left unfinished by a previous process`);
229
+ for (const task of unowned)
230
+ await this.harness.abortTask(task.record.id, context);
231
+ }
232
+ async index() {
233
+ return (await this.harness.snapshot(SessionIndexDoc, context))?.sessions ?? {};
234
+ }
235
+ claimWriter(conversationId, key) {
236
+ if (this.writers.has(conversationId)) {
237
+ throw new Error(`Session already has an active writer: ${key}`);
238
+ }
239
+ this.writers.add(conversationId);
99
240
  }
100
- if (!isMetadataRecord(parsed)) {
101
- throw new SessionFormatError(`Session file has an unrecognized header: ${filePath}`);
241
+ releaseWriter(conversationId) {
242
+ this.writers.delete(conversationId);
102
243
  }
103
- const record = parsed;
104
- if (record.kind !== "header" ||
105
- record.v !== CURRENT_SESSION_VERSION ||
106
- record.storageVersion !== JSONL_STORAGE_VERSION ||
107
- typeof record.id !== "string" ||
108
- typeof record.createdAt !== "number" ||
109
- typeof record.cwd !== "string") {
110
- throw new SessionFormatError(`Session file has an unrecognized header: ${filePath}`);
244
+ bind(conversationId, binding) {
245
+ if (this.bindings.has(conversationId)) {
246
+ throw new Error("Session already has an attached harness");
247
+ }
248
+ this.bindings.set(conversationId, binding);
249
+ }
250
+ unbind(conversationId) {
251
+ this.bindings.delete(conversationId);
252
+ }
253
+ tag(conversationId, messages) {
254
+ const last = messages.at(-1);
255
+ if (!last)
256
+ return messages;
257
+ const tagged = { ...last };
258
+ this.owners.set(tagged, conversationId);
259
+ return [...messages.slice(0, -1), tagged];
260
+ }
261
+ async release() {
262
+ this.refs -= 1;
263
+ if (this.refs > 0)
264
+ return;
265
+ if (this.path !== null)
266
+ OfficeStorage.pool.delete(this.path);
267
+ await this.harness.close(context);
111
268
  }
112
- return parsed;
113
269
  }
114
- function metadataFromHeader(header, path) {
115
- return {
116
- id: header.id,
117
- createdAt: header.createdAt,
118
- storageVersion: header.storageVersion,
119
- cwd: header.cwd,
120
- path,
121
- modifiedAt: 0,
122
- parentSessionId: header.parentSessionId,
123
- legacyParentSessionPath: header.legacyParentSessionPath,
270
+ function writeSessionIndex(tx, key, record) {
271
+ return tx.doc(SessionIndexDoc).then((doc) => {
272
+ doc.sessions[key] = record;
273
+ });
274
+ }
275
+ async function lookupSession(storage, key) {
276
+ const record = (await storage.index())[key];
277
+ if (!record)
278
+ return undefined;
279
+ const conversation = await storage.harness.conversation(record.conversationId, context);
280
+ return conversation ? { conversation, record } : undefined;
281
+ }
282
+ async function createSession(storage, key) {
283
+ let record;
284
+ const init = async (tx, conversationId) => {
285
+ record = { conversationId, id: randomUUID(), createdAt: Date.now() };
286
+ await writeSessionIndex(tx, key, record);
124
287
  };
288
+ const conversation = isThreadSessionKey(key)
289
+ ? await storage.harness.createConversation({ ownership: { kind: "ownerless" }, init }, context)
290
+ : await storage.harness.root(context, { init });
291
+ if (!record) {
292
+ record = { conversationId: conversation.id, id: randomUUID(), createdAt: Date.now() };
293
+ const created = record;
294
+ await conversation.commit((tx) => writeSessionIndex(tx, key, created), context);
295
+ }
296
+ return { conversation, record };
125
297
  }
126
- function parseMikanMetadata(filePath) {
127
- let current;
128
- for (const line of readFileSync(filePath, "utf-8").split("\n").slice(1)) {
129
- for (const write of parseMetadataWrites(line)) {
130
- if (write.op === "delete")
131
- current = undefined;
132
- else if (write.op === "set" && isMetadataRecord(write.value))
133
- current = write.value;
134
- }
298
+ async function parentSessionId(storage, conversationId) {
299
+ const parent = await storage.harness.commit(async (tx) => (await tx.conversation(conversationId))?.parent, context);
300
+ if (!parent)
301
+ return undefined;
302
+ const records = Object.values(await storage.index());
303
+ return records.find((record) => record.conversationId === parent.conversationId)?.id;
304
+ }
305
+ function isRunCause(record, messageId) {
306
+ if (!CustomSessionEntry.is(record) || record.data.customType !== RUN_CAUSE_CUSTOM_TYPE) {
307
+ return false;
135
308
  }
136
- return current;
309
+ const { data } = record.data;
310
+ return messageId === undefined || (isRecord(data) && data.messageId === messageId);
137
311
  }
138
- function isMetadataRecord(input) {
139
- return isRecord(input);
312
+ function runEnd(records, messageId) {
313
+ const start = records.findLastIndex((record) => isRunCause(record, messageId));
314
+ if (start < 0)
315
+ return undefined;
316
+ const next = records.findIndex((record, index) => index > start && isRunCause(record));
317
+ const run = records.slice(start, next < 0 ? undefined : next);
318
+ return (run.findLast((record) => AssistantEntry.is(record)) ?? run.at(-1))?.id;
140
319
  }
141
- function parseMetadataWrites(line) {
142
- let transaction;
143
- try {
144
- transaction = JSON.parse(line);
145
- }
146
- catch {
147
- return [];
148
- }
149
- const writes = Array.isArray(transaction) ? transaction : [transaction];
150
- return writes.filter((write) => isMetadataRecord(write) &&
151
- write.kind === "value" &&
152
- write.namespace === "mikan" &&
153
- write.key === "metadata");
320
+ const EARLIER_SESSION_PREFIX = "earlier:";
321
+ export function earlierSessionKey(sessionId) {
322
+ return `${EARLIER_SESSION_PREFIX}${sessionId}`;
154
323
  }
155
- function fileFingerprint(path) {
156
- const fd = openSync(path, "r");
157
- try {
158
- const stats = fstatSync(fd);
159
- return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}`;
160
- }
161
- finally {
162
- closeSync(fd);
163
- }
324
+ export function isEarlierSessionKey(key) {
325
+ return key.startsWith(EARLIER_SESSION_PREFIX);
164
326
  }
165
- function materializePendingFile(path, header, expectedFingerprint) {
166
- const content = `${JSON.stringify(header)}\n`;
167
- if (expectedFingerprint === null) {
168
- mkdirSync(dirname(path), { recursive: true });
169
- const fd = openSync(path, "wx", 0o600);
170
- try {
171
- writeFileSync(fd, content);
172
- }
173
- finally {
174
- closeSync(fd);
327
+ async function appendImportedEntry(tx, conversationId, entry) {
328
+ switch (entry.type) {
329
+ case "message": {
330
+ const model = [durableMessage(entry.message)];
331
+ if (entry.message.role === "user")
332
+ await tx.appendEntry(UserEntry, conversationId, { model });
333
+ else if (entry.message.role === "assistant") {
334
+ await tx.appendEntry(AssistantEntry, conversationId, { model });
335
+ }
336
+ else {
337
+ await tx.appendEntry(ToolResultEntry, conversationId, {
338
+ model,
339
+ data: { diagnostics: [] },
340
+ });
341
+ }
342
+ return;
175
343
  }
176
- return;
344
+ case "compaction":
345
+ await tx.appendEntry(CompactionEntry, conversationId, {
346
+ model: [
347
+ {
348
+ role: "user",
349
+ content: [{ type: "text", text: wrapCompactionSummary(entry.summary) }],
350
+ timestamp: entry.timestamp,
351
+ },
352
+ ],
353
+ data: { reason: "manual" },
354
+ head: "self",
355
+ });
356
+ return;
357
+ case "custom":
358
+ await tx.appendEntry(CustomSessionEntry, conversationId, {
359
+ data: {
360
+ customType: entry.customType,
361
+ data: toJson(entry.data),
362
+ timestamp: entry.timestamp,
363
+ },
364
+ });
365
+ return;
366
+ default:
367
+ entry;
177
368
  }
178
- const fd = openSync(path, "r+");
369
+ }
370
+ export async function importOfficeSessions(path, sessions) {
371
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
372
+ const harness = await Harness.open(await openNodeSqliteStorage(path), { models: {}, registry: createRegistry(), settings: { extensions: [] } }, context);
179
373
  try {
180
- const stats = fstatSync(fd);
181
- const actual = `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}`;
182
- if (actual !== expectedFingerprint) {
183
- throw new Error(`Session file changed before pending session materialization: ${path}`);
374
+ for (const session of sessions) {
375
+ const init = async (tx, conversationId) => {
376
+ for (const entry of session.entries)
377
+ await appendImportedEntry(tx, conversationId, entry);
378
+ if (session.name)
379
+ (await tx.doc(SessionDoc, conversationId)).name = session.name;
380
+ await writeSessionIndex(tx, session.key, {
381
+ conversationId,
382
+ id: session.id,
383
+ createdAt: session.createdAt,
384
+ });
385
+ };
386
+ if (session.root)
387
+ await harness.root(context, { init });
388
+ else
389
+ await harness.createConversation({ ownership: { kind: "ownerless" }, init }, context);
184
390
  }
185
- ftruncateSync(fd, 0);
186
- writeFileSync(fd, content);
187
391
  }
188
392
  finally {
189
- closeSync(fd);
393
+ await harness.close(context);
190
394
  }
395
+ protectStorageFiles(path);
191
396
  }
192
- function buildHeader(cwd, options) {
193
- const metadata = options?.parentSession === undefined
194
- ? undefined
195
- : { parentSessionPath: options.parentSession };
196
- return {
197
- header: {
198
- v: CURRENT_SESSION_VERSION,
199
- kind: "header",
200
- id: options?.id ?? uuidv7(),
201
- storageVersion: JSONL_STORAGE_VERSION,
202
- createdAt: Date.now(),
203
- cwd,
204
- parentSessionId: options?.parentSessionId,
205
- legacyParentSessionPath: options?.parentSession,
206
- },
207
- metadata,
208
- };
209
- }
210
- function headerView(header, metadata) {
211
- const parentSession = typeof metadata?.parentSessionPath === "string"
212
- ? metadata.parentSessionPath
213
- : header.legacyParentSessionPath;
214
- return {
215
- type: "session",
216
- version: CURRENT_SESSION_VERSION,
217
- id: header.id,
218
- timestamp: new Date(header.createdAt).toISOString(),
219
- cwd: header.cwd,
220
- parentSessionId: header.parentSessionId,
221
- parentSession,
222
- metadata,
223
- };
224
- }
225
- async function openFileSession(path, header) {
226
- const sessionRepo = new JsonlSessionRepo({
227
- fileSystem: new NodeExecutionEnv({ cwd: process.cwd() }),
228
- sessionsRoot: dirname(path),
229
- });
230
- try {
231
- const session = await sessionRepo.open(metadataFromHeader(header, path), TODO_CONTEXT);
232
- return { session, repo: sessionRepo };
233
- }
234
- catch (error) {
235
- await sessionRepo.close(TODO_CONTEXT);
236
- throw error;
237
- }
238
- }
239
- async function withSessionSnapshot(path, read) {
240
- const resolvedPath = canonicalSessionPath(path);
241
- const dir = mkdtempSync(join(tmpdir(), "mikan-session-inspect-"));
242
- const snapshot = join(dir, basename(resolvedPath));
243
- let opened;
397
+ export async function readImportedContexts(path) {
398
+ const harness = await Harness.open(await openNodeSqliteStorage(path), { models: {}, registry: createRegistry(), settings: { extensions: [] } }, context);
244
399
  try {
245
- writeFileSync(snapshot, readFileSync(resolvedPath), { mode: 0o600, flag: "wx" });
246
- const header = parseCurrentHeader(resolvedPath, readHeaderLine(snapshot));
247
- opened = await openFileSession(snapshot, header);
248
- return await read(opened.session, header);
249
- }
250
- finally {
251
- try {
252
- await opened?.session.close(TODO_CONTEXT);
253
- }
254
- finally {
255
- try {
256
- await opened?.repo.close(TODO_CONTEXT);
257
- }
258
- finally {
259
- rmSync(dir, { recursive: true, force: true });
260
- }
400
+ const index = (await harness.snapshot(SessionIndexDoc, context))?.sessions ?? {};
401
+ const contexts = new Map();
402
+ for (const [key, record] of Object.entries(index)) {
403
+ const conversation = await harness.conversation(record.conversationId, context);
404
+ if (conversation)
405
+ contexts.set(key, (await readContext(conversation)).messages);
261
406
  }
407
+ return contexts;
262
408
  }
263
- }
264
- async function mainBranch(session, create) {
265
- const existing = await session.branch("main", TODO_CONTEXT);
266
- if (existing || !create)
267
- return existing;
268
- return session.createBranch("main", null, TODO_CONTEXT);
269
- }
270
- function isContextMessage(message) {
271
- return (message.role !== "assistant" ||
272
- (message.stopReason !== "error" &&
273
- message.stopReason !== "aborted" &&
274
- message.stopReason !== "deferred"));
275
- }
276
- function buildContext(entries) {
277
- const compactionIndex = entries.findLastIndex((entry) => entry.type === "compaction");
278
- const visibleEntries = entries.slice(Math.max(0, compactionIndex));
279
- const messages = [];
280
- for (const entry of visibleEntries) {
281
- if (entry.type === "message") {
282
- if (isContextMessage(entry.message))
283
- messages.push(entry.message);
284
- }
285
- else if (entry.type === "compaction") {
286
- messages.push(createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp), ...entry.retainedTail.filter(isContextMessage));
287
- }
288
- else if (entry.type === "branch_summary" && entry.summary) {
289
- messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
290
- }
409
+ finally {
410
+ await harness.close(context);
291
411
  }
292
- return { messages };
293
412
  }
294
413
  class CachedSessionInspection {
295
- constructor(header, entries, name, branch, context) {
414
+ constructor(header, entries, name, sessionContext) {
296
415
  this.header = header;
297
416
  this.entries = entries;
298
417
  this.name = name;
299
- this.branch = branch;
300
- this.context = context;
418
+ this.sessionContext = sessionContext;
301
419
  }
302
420
  getHeader() {
303
421
  return structuredClone(this.header);
@@ -308,190 +426,173 @@ class CachedSessionInspection {
308
426
  async getSessionName() {
309
427
  return this.name;
310
428
  }
311
- async getBranch(fromId) {
312
- if (fromId === undefined)
313
- return structuredClone(this.branch);
314
- const byId = new Map(this.entries.map((entry) => [entry.id, entry]));
315
- const result = [];
316
- let current = byId.get(fromId);
317
- while (current) {
318
- result.push(current);
319
- current = current.parentId === null ? undefined : byId.get(current.parentId);
320
- }
321
- return result.toReversed();
322
- }
323
429
  async buildSessionContext() {
324
- return structuredClone(this.context);
430
+ return structuredClone(this.sessionContext);
431
+ }
432
+ }
433
+ async function withOfficeStorage(office, read) {
434
+ const storage = await OfficeStorage.acquire(office.sessionsPath);
435
+ try {
436
+ return await read(storage);
437
+ }
438
+ finally {
439
+ await storage.release();
325
440
  }
326
441
  }
327
442
  export class SessionStore {
328
- constructor(sessionFile, cwd, state, writerKey) {
329
- this.sessionFile = sessionFile;
330
- this.cwd = cwd;
331
- this.state = state;
332
- this.writerKey = writerKey;
443
+ constructor(storage, conversation, header) {
444
+ this.storage = storage;
445
+ this.conversation = conversation;
446
+ this.header = header;
333
447
  this.mutationTail = Promise.resolve();
334
448
  this.closed = false;
449
+ this.bound = false;
335
450
  }
336
- static async open(path, cwdOverride) {
337
- const writer = acquireWriter(path);
338
- const writerPath = writer.path;
451
+ static async open(office, key) {
452
+ const storage = await OfficeStorage.acquire(office.sessionsPath);
339
453
  try {
340
- if (!existsSync(writerPath)) {
341
- const cwd = cwdOverride ?? process.cwd();
342
- const built = buildHeader(cwd);
343
- return new SessionStore(writerPath, cwd, { kind: "pending", ...built, fingerprint: null }, writer.key);
344
- }
345
- const firstLine = readHeaderLine(writerPath);
346
- if (firstLine.trim().length === 0) {
347
- const cwd = cwdOverride ?? process.cwd();
348
- const built = buildHeader(cwd);
349
- return new SessionStore(writerPath, cwd, { kind: "pending", ...built, fingerprint: fileFingerprint(writerPath) }, writer.key);
350
- }
351
- const header = parseCurrentHeader(writerPath, firstLine);
352
- const opened = await openFileSession(writerPath, header);
353
- const metadata = (await opened.session.getValue(MIKAN_METADATA, TODO_CONTEXT))?.value;
354
- return new SessionStore(writerPath, cwdOverride ?? header.cwd, { kind: "live", header, metadata, ...opened }, writer.key);
454
+ const session = (await lookupSession(storage, key)) ?? (await createSession(storage, key));
455
+ storage.claimWriter(session.conversation.id, key);
456
+ return new SessionStore(storage, session.conversation, {
457
+ id: session.record.id,
458
+ createdAt: session.record.createdAt,
459
+ parentSessionId: await parentSessionId(storage, session.conversation.id),
460
+ });
355
461
  }
356
462
  catch (error) {
357
- releaseWriter(writer.key);
463
+ await storage.release();
358
464
  throw error;
359
465
  }
360
466
  }
361
- static async inspectExecution(path) {
362
- return withSessionSnapshot(path, async (session) => {
363
- const state = (await session.getValue(laneState("main"), TODO_CONTEXT))?.value;
364
- const result = state?.lastOperationId
365
- ? (await session.getValue(operationResult(state.lastOperationId), TODO_CONTEXT))?.value
366
- : undefined;
367
- return {
368
- open: state?.currentOperationId != null,
369
- started: state?.currentOperationId != null || state?.lastOperationId != null,
370
- result: result ? { status: result.status, endedAt: result.endedAt } : undefined,
371
- };
467
+ static async forkRun(office, key, cause, entries) {
468
+ return withOfficeStorage(office, async (storage) => {
469
+ if ((await storage.index())[key])
470
+ return false;
471
+ const parent = await lookupSession(storage, cause.sessionKey);
472
+ if (!parent)
473
+ return false;
474
+ const at = runEnd(await readRecords(parent.conversation), cause.messageId);
475
+ if (at === undefined)
476
+ return false;
477
+ await parent.conversation.fork(at, {
478
+ ownership: { kind: "ownerless" },
479
+ init: async (tx, conversationId) => {
480
+ for (const entry of entries)
481
+ await appendImportedEntry(tx, conversationId, entry);
482
+ await writeSessionIndex(tx, key, {
483
+ conversationId,
484
+ id: randomUUID(),
485
+ createdAt: Date.now(),
486
+ });
487
+ },
488
+ }, context);
489
+ return true;
372
490
  });
373
491
  }
374
- static async inspect(path) {
375
- return withSessionSnapshot(path, async (session, header) => {
376
- const metadata = (await session.getValue(MIKAN_METADATA, TODO_CONTEXT))?.value;
377
- const entries = await session.findEntries({ order: "asc" }, TODO_CONTEXT);
378
- const name = await session.getName(TODO_CONTEXT);
379
- const branchObject = await mainBranch(session, false);
380
- const branch = branchObject
381
- ? await branchObject.findEntries({ order: "oldestFirst" }, TODO_CONTEXT)
382
- : [];
383
- const context = buildContext(branch);
384
- return new CachedSessionInspection(headerView(header, metadata), entries, name, branch, context);
385
- });
492
+ static async inMemory() {
493
+ const storage = await OfficeStorage.inMemory();
494
+ const conversation = await storage.harness.root(context);
495
+ return new SessionStore(storage, conversation, { id: randomUUID(), createdAt: Date.now() });
386
496
  }
387
- static async create(path, cwd, options) {
388
- const writer = acquireWriter(path);
389
- const writerPath = writer.path;
390
- let leaseKey = writer.key;
391
- let opened;
392
- try {
393
- const built = buildHeader(cwd, options);
394
- mkdirSync(dirname(writerPath), { recursive: true });
395
- writeFileSync(writerPath, `${JSON.stringify(built.header)}\n`, { mode: 0o600, flag: "wx" });
396
- leaseKey = promotePendingWriter(leaseKey, writerPath);
397
- opened = await openFileSession(writerPath, built.header);
398
- if (built.metadata) {
399
- await opened.session.setValue(MIKAN_METADATA, built.metadata, TODO_CONTEXT);
400
- }
401
- return new SessionStore(writerPath, cwd, { kind: "live", ...built, ...opened }, leaseKey);
402
- }
403
- catch (error) {
404
- if (opened) {
405
- await opened.session.close(TODO_CONTEXT).catch(() => undefined);
406
- await opened.repo.close(TODO_CONTEXT).catch(() => undefined);
407
- }
408
- releaseWriter(leaseKey);
409
- throw error;
410
- }
497
+ static async exists(office, key) {
498
+ if (!existsSync(office.sessionsPath))
499
+ return false;
500
+ return withOfficeStorage(office, async (storage) => (await storage.index())[key] !== undefined);
411
501
  }
412
- static readHeader(path) {
413
- let firstLine;
414
- try {
415
- firstLine = readHeaderLine(path);
416
- }
417
- catch {
418
- return null;
419
- }
420
- if (firstLine.trim().length === 0)
421
- return null;
422
- try {
423
- const header = parseCurrentHeader(path, firstLine);
424
- return headerView(header, parseMikanMetadata(path));
425
- }
426
- catch {
427
- return null;
428
- }
429
- }
430
- static writeHeaderFile(path, cwd, options) {
431
- mkdirSync(dirname(path), { recursive: true });
432
- const built = buildHeader(cwd, options);
433
- atomicWritePrivateFile(path, `${JSON.stringify(built.header)}\n`);
434
- }
435
- static inMemory(cwd = process.cwd()) {
436
- const built = buildHeader(cwd);
437
- return new SessionStore(null, cwd, { kind: "pending", ...built, fingerprint: null }, null);
438
- }
439
- getSessionFile() {
440
- this.assertOpen();
441
- return this.sessionFile ?? undefined;
502
+ static async list(office) {
503
+ if (!existsSync(office.sessionsPath))
504
+ return [];
505
+ return withOfficeStorage(office, async (storage) => {
506
+ const index = await storage.index();
507
+ const byConversation = new Map(Object.values(index).map((record) => [record.conversationId, record.id]));
508
+ return Promise.all(Object.entries(index).map(async ([key, record]) => {
509
+ const parent = await storage.harness.commit(async (tx) => (await tx.conversation(record.conversationId))?.parent, context);
510
+ const parentId = parent ? byConversation.get(parent.conversationId) : undefined;
511
+ return {
512
+ key,
513
+ id: record.id,
514
+ createdAt: record.createdAt,
515
+ root: record.conversationId === ROOT_CONVERSATION_ID,
516
+ parentSessionId: parentId,
517
+ forkEntryId: parent && parentId ? `${parentId}:${parent.at}` : undefined,
518
+ };
519
+ }));
520
+ });
442
521
  }
443
- isPersisted() {
444
- this.assertOpen();
445
- return this.sessionFile !== null;
522
+ static async inspect(office, key) {
523
+ if (!existsSync(office.sessionsPath))
524
+ return undefined;
525
+ return withOfficeStorage(office, async (storage) => {
526
+ const session = await lookupSession(storage, key);
527
+ if (!session)
528
+ return undefined;
529
+ const { conversation, record } = session;
530
+ const name = (await storage.harness.snapshot(SessionDoc, conversation.id, context))?.name;
531
+ return new CachedSessionInspection({
532
+ id: record.id,
533
+ createdAt: record.createdAt,
534
+ parentSessionId: await parentSessionId(storage, conversation.id),
535
+ }, toSessionEntries(await readRecords(conversation), record.id), name, await readContext(conversation));
536
+ });
446
537
  }
447
- getCwd() {
448
- this.assertOpen();
449
- return this.cwd;
538
+ static async inspectExecution(office, key) {
539
+ if (!existsSync(office.sessionsPath))
540
+ return { open: false, started: false };
541
+ return withOfficeStorage(office, async (storage) => {
542
+ const session = await lookupSession(storage, key);
543
+ if (!session)
544
+ return { open: false, started: false };
545
+ const id = session.conversation.id;
546
+ const live = await storage.harness.snapshot(LiveDoc, id, context);
547
+ const run = (await storage.harness.snapshot(SessionDoc, id, context))?.run;
548
+ const open = live?.run !== undefined;
549
+ return {
550
+ open,
551
+ started: open || run !== undefined,
552
+ result: run?.endedAt !== undefined && run.status !== undefined
553
+ ? { status: run.status, endedAt: run.endedAt }
554
+ : undefined,
555
+ };
556
+ });
450
557
  }
451
558
  getHeader() {
452
559
  this.assertOpen();
453
- return headerView(this.state.header, this.state.metadata);
560
+ return structuredClone(this.header);
454
561
  }
455
562
  getSessionId() {
456
563
  this.assertOpen();
457
- return this.state.header.id;
564
+ return this.header.id;
458
565
  }
459
- async getLeafId() {
566
+ async getEntries() {
460
567
  this.assertOpen();
461
- if (this.state.kind === "pending")
462
- return null;
463
- return (await mainBranch(this.state.session, false))?.getTipId(TODO_CONTEXT) ?? null;
568
+ return toSessionEntries(await readRecords(this.conversation), this.header.id);
464
569
  }
465
- async getEntry(id) {
570
+ async getContextEntries() {
466
571
  this.assertOpen();
467
- if (this.state.kind === "pending")
468
- return undefined;
469
- return this.state.session.getEntry(id, TODO_CONTEXT);
572
+ const view = await this.conversation.context(context);
573
+ return toSessionEntries(view.entries, this.header.id);
470
574
  }
471
- async getEntries() {
575
+ async getContextTokens() {
472
576
  this.assertOpen();
473
- if (this.state.kind === "pending")
474
- return [];
475
- return this.state.session.findEntries({ order: "asc" }, TODO_CONTEXT);
577
+ const view = await this.conversation.context(context);
578
+ const after = view.head?.id ?? Number.NEGATIVE_INFINITY;
579
+ for (const record of view.entries.toReversed()) {
580
+ const message = record.model?.[0];
581
+ if (record.id <= after || !AssistantEntry.is(record) || message?.role !== "assistant")
582
+ continue;
583
+ if (message.stopReason === "aborted" || message.stopReason === "error")
584
+ continue;
585
+ return calculateContextTokens(message.usage);
586
+ }
587
+ return undefined;
476
588
  }
477
589
  async getSessionName() {
478
590
  this.assertOpen();
479
- if (this.state.kind === "pending")
480
- return undefined;
481
- return this.state.session.getName(TODO_CONTEXT);
482
- }
483
- async getBranch(fromId) {
484
- this.assertOpen();
485
- if (this.state.kind === "pending")
486
- return [];
487
- if (fromId !== undefined) {
488
- return this.state.session.scanBranch({ start: fromId, order: "oldestFirst" }, TODO_CONTEXT);
489
- }
490
- const branchObject = await mainBranch(this.state.session, false);
491
- return branchObject ? branchObject.findEntries({ order: "oldestFirst" }, TODO_CONTEXT) : [];
591
+ return (await this.storage.harness.snapshot(SessionDoc, this.conversation.id, context))?.name;
492
592
  }
493
593
  async buildSessionContext() {
494
- return buildContext(await this.getBranch());
594
+ this.assertOpen();
595
+ return readContext(this.conversation);
495
596
  }
496
597
  async connectMcp(servers, signal) {
497
598
  return this.mutate(async () => {
@@ -509,73 +610,104 @@ export class SessionStore {
509
610
  const instructions = formatMcpServerInstructions(this.mcp?.instructions ?? []);
510
611
  return instructions ? `${prompt}\n\n${instructions}` : prompt;
511
612
  }
512
- async createHarness(options) {
613
+ async bindHarness(binding) {
513
614
  return this.mutate(async () => {
514
- if (this.harness)
515
- throw new Error("SessionStore already has an attached harness");
516
- const { session } = await this.live();
517
- const { harness } = await AgentHarness.create({ ...options, session }, TODO_CONTEXT);
518
- this.harness = harness;
519
- this.harnessLane = await harness.lane("main", TODO_CONTEXT);
520
- return harness;
615
+ this.storage.bind(this.conversation.id, binding);
616
+ this.bound = true;
617
+ const conversationId = this.conversation.id;
618
+ return {
619
+ harness: this.storage.harness,
620
+ conversation: this.conversation,
621
+ registry: this.storage.registry,
622
+ extensionName: `mikan.${conversationId}`,
623
+ tagRequest: (messages) => this.storage.tag(conversationId, messages),
624
+ };
521
625
  });
522
626
  }
523
627
  async appendMessage(message) {
524
628
  return this.mutate(async () => {
525
- if (this.harnessLane)
526
- return this.harnessLane.appendMessage(toDurable(message), TODO_CONTEXT);
527
- const branchObject = await mainBranch((await this.live()).session, true);
528
- if (!branchObject)
529
- throw new Error("Failed to create main session branch");
530
- return branchObject.appendMessage(toDurable(message), TODO_CONTEXT);
629
+ const conversation = this.conversation;
630
+ const model = [durableMessage(message)];
631
+ const entry = await conversation.commit((tx) => {
632
+ switch (message.role) {
633
+ case "user":
634
+ return tx.appendEntry(UserEntry, conversation.id, { model });
635
+ case "assistant":
636
+ return tx.appendEntry(AssistantEntry, conversation.id, { model });
637
+ case "toolResult":
638
+ return tx.appendEntry(ToolResultEntry, conversation.id, {
639
+ model,
640
+ data: { diagnostics: [] },
641
+ });
642
+ default:
643
+ throw new Error("Only user, assistant, and tool result messages enter a session");
644
+ }
645
+ }, context);
646
+ return `${this.header.id}:${entry.id}`;
531
647
  });
532
648
  }
533
- async appendCustomEntry(customType, data) {
649
+ async appendCustomEntry(customType, data, timestamp = Date.now()) {
534
650
  return this.mutate(async () => {
535
- if (this.harnessLane) {
536
- return this.harnessLane.appendCustomEntry(customType, toDurable(data), TODO_CONTEXT);
537
- }
538
- const branchObject = await mainBranch((await this.live()).session, true);
539
- if (!branchObject)
540
- throw new Error("Failed to create main session branch");
541
- return branchObject.appendCustomEntry(customType, toDurable(data), TODO_CONTEXT);
651
+ const conversation = this.conversation;
652
+ const entry = await conversation.commit((tx) => tx.appendEntry(CustomSessionEntry, conversation.id, {
653
+ data: { customType, data: toJson(data), timestamp },
654
+ }), context);
655
+ return `${this.header.id}:${entry.id}`;
656
+ });
657
+ }
658
+ async appendCompactionSummary(summary, timestamp) {
659
+ const text = wrapCompactionSummary(summary);
660
+ return this.mutate(async () => {
661
+ const conversation = this.conversation;
662
+ const entry = await conversation.commit((tx) => tx.appendEntry(CompactionEntry, conversation.id, {
663
+ model: [{ role: "user", content: [{ type: "text", text }], timestamp }],
664
+ data: { reason: "manual" },
665
+ head: "self",
666
+ }), context);
667
+ return `${this.header.id}:${entry.id}`;
542
668
  });
543
669
  }
544
- async appendCustomMessageEntry(customType, content, display, details) {
545
- return this.appendMessage(toDurable(createCustomMessage(customType, content, display, details, Date.now())));
670
+ async reset() {
671
+ await this.mutate(() => this.conversation.reset(undefined, context));
546
672
  }
547
673
  async setSessionName(name) {
548
- await this.mutate(async () => (await this.live()).session.setName(name.trim() || undefined, TODO_CONTEXT));
674
+ await this.updateSessionDoc((doc) => {
675
+ doc.name = name.trim() || undefined;
676
+ });
677
+ }
678
+ async recordRun(run) {
679
+ await this.updateSessionDoc((doc) => {
680
+ doc.run = { ...run };
681
+ });
549
682
  }
550
683
  async close() {
551
684
  if (this.closePromise)
552
685
  return this.closePromise;
553
686
  this.closed = true;
554
- this.closePromise = this.mutationTail
555
- .then(async () => {
687
+ const conversationId = this.conversation.id;
688
+ this.closePromise = this.mutationTail.then(async () => {
556
689
  try {
557
690
  await this.mcp?.dispose();
558
691
  }
559
692
  finally {
560
- if (this.state.kind === "live") {
561
- try {
562
- if (this.harness)
563
- await this.harness.close(TODO_CONTEXT);
564
- else
565
- await this.state.session.close(TODO_CONTEXT);
566
- }
567
- finally {
568
- await this.state.repo?.close(TODO_CONTEXT);
569
- }
693
+ if (this.bound) {
694
+ this.storage.registry.uninstall({ name: `mikan.${conversationId}` });
695
+ this.storage.unbind(conversationId);
570
696
  }
697
+ this.storage.releaseWriter(conversationId);
698
+ await this.storage.release();
571
699
  }
572
- })
573
- .finally(() => {
574
- if (this.writerKey !== null)
575
- releaseWriter(this.writerKey);
576
700
  });
577
701
  return this.closePromise;
578
702
  }
703
+ async updateSessionDoc(change) {
704
+ await this.mutate(async () => {
705
+ const conversation = this.conversation;
706
+ await conversation.commit(async (tx) => {
707
+ change(await tx.doc(SessionDoc, conversation.id));
708
+ }, context);
709
+ });
710
+ }
579
711
  assertOpen() {
580
712
  if (this.closed)
581
713
  throw new Error("SessionStore is closed");
@@ -586,43 +718,5 @@ export class SessionStore {
586
718
  this.mutationTail = result.then(() => undefined, () => undefined);
587
719
  return result;
588
720
  }
589
- async live() {
590
- if (this.state.kind === "live")
591
- return this.state;
592
- const pending = this.state;
593
- const sessionFile = this.sessionFile;
594
- if (sessionFile === null) {
595
- const repo = new MemorySessionRepo();
596
- const session = await repo.create({
597
- id: pending.header.id,
598
- parentSessionId: pending.header.parentSessionId,
599
- }, TODO_CONTEXT);
600
- const live = {
601
- kind: "live",
602
- header: pending.header,
603
- metadata: pending.metadata,
604
- session,
605
- repo,
606
- };
607
- this.state = live;
608
- return live;
609
- }
610
- materializePendingFile(sessionFile, pending.header, pending.fingerprint);
611
- if (this.writerKey === null)
612
- throw new Error("Pending session must have a writer lease");
613
- this.writerKey = promotePendingWriter(this.writerKey, sessionFile);
614
- const opened = await openFileSession(sessionFile, pending.header);
615
- if (pending.metadata) {
616
- await opened.session.setValue(MIKAN_METADATA, pending.metadata, TODO_CONTEXT);
617
- }
618
- const live = {
619
- kind: "live",
620
- header: pending.header,
621
- metadata: pending.metadata,
622
- ...opened,
623
- };
624
- this.state = live;
625
- return live;
626
- }
627
721
  }
628
722
  //# sourceMappingURL=session-store.js.map