@geminixiang/mikan 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/LICENSE +22 -0
  3. package/README.md +297 -0
  4. package/dist/adapter.d.ts +134 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +2 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/adapters/discord/bot.d.ts +63 -0
  9. package/dist/adapters/discord/bot.d.ts.map +1 -0
  10. package/dist/adapters/discord/bot.js +577 -0
  11. package/dist/adapters/discord/bot.js.map +1 -0
  12. package/dist/adapters/discord/context.d.ts +9 -0
  13. package/dist/adapters/discord/context.d.ts.map +1 -0
  14. package/dist/adapters/discord/context.js +245 -0
  15. package/dist/adapters/discord/context.js.map +1 -0
  16. package/dist/adapters/discord/index.d.ts +3 -0
  17. package/dist/adapters/discord/index.d.ts.map +1 -0
  18. package/dist/adapters/discord/index.js +3 -0
  19. package/dist/adapters/discord/index.js.map +1 -0
  20. package/dist/adapters/shared.d.ts +91 -0
  21. package/dist/adapters/shared.d.ts.map +1 -0
  22. package/dist/adapters/shared.js +191 -0
  23. package/dist/adapters/shared.js.map +1 -0
  24. package/dist/adapters/slack/bot.d.ts +139 -0
  25. package/dist/adapters/slack/bot.d.ts.map +1 -0
  26. package/dist/adapters/slack/bot.js +1272 -0
  27. package/dist/adapters/slack/bot.js.map +1 -0
  28. package/dist/adapters/slack/branch-manager.d.ts +28 -0
  29. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  30. package/dist/adapters/slack/branch-manager.js +117 -0
  31. package/dist/adapters/slack/branch-manager.js.map +1 -0
  32. package/dist/adapters/slack/context.d.ts +12 -0
  33. package/dist/adapters/slack/context.d.ts.map +1 -0
  34. package/dist/adapters/slack/context.js +327 -0
  35. package/dist/adapters/slack/context.js.map +1 -0
  36. package/dist/adapters/slack/index.d.ts +3 -0
  37. package/dist/adapters/slack/index.d.ts.map +1 -0
  38. package/dist/adapters/slack/index.js +3 -0
  39. package/dist/adapters/slack/index.js.map +1 -0
  40. package/dist/adapters/slack/session.d.ts +38 -0
  41. package/dist/adapters/slack/session.d.ts.map +1 -0
  42. package/dist/adapters/slack/session.js +66 -0
  43. package/dist/adapters/slack/session.js.map +1 -0
  44. package/dist/adapters/slack/tools/attach.d.ts +12 -0
  45. package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
  46. package/dist/adapters/slack/tools/attach.js +40 -0
  47. package/dist/adapters/slack/tools/attach.js.map +1 -0
  48. package/dist/adapters/telegram/bot.d.ts +51 -0
  49. package/dist/adapters/telegram/bot.d.ts.map +1 -0
  50. package/dist/adapters/telegram/bot.js +430 -0
  51. package/dist/adapters/telegram/bot.js.map +1 -0
  52. package/dist/adapters/telegram/context.d.ts +9 -0
  53. package/dist/adapters/telegram/context.d.ts.map +1 -0
  54. package/dist/adapters/telegram/context.js +190 -0
  55. package/dist/adapters/telegram/context.js.map +1 -0
  56. package/dist/adapters/telegram/html.d.ts +3 -0
  57. package/dist/adapters/telegram/html.d.ts.map +1 -0
  58. package/dist/adapters/telegram/html.js +98 -0
  59. package/dist/adapters/telegram/html.js.map +1 -0
  60. package/dist/adapters/telegram/index.d.ts +3 -0
  61. package/dist/adapters/telegram/index.d.ts.map +1 -0
  62. package/dist/adapters/telegram/index.js +3 -0
  63. package/dist/adapters/telegram/index.js.map +1 -0
  64. package/dist/agent.d.ts +36 -0
  65. package/dist/agent.d.ts.map +1 -0
  66. package/dist/agent.js +1147 -0
  67. package/dist/agent.js.map +1 -0
  68. package/dist/commands/auto-reply.d.ts +5 -0
  69. package/dist/commands/auto-reply.d.ts.map +1 -0
  70. package/dist/commands/auto-reply.js +79 -0
  71. package/dist/commands/auto-reply.js.map +1 -0
  72. package/dist/commands/index.d.ts +5 -0
  73. package/dist/commands/index.d.ts.map +1 -0
  74. package/dist/commands/index.js +18 -0
  75. package/dist/commands/index.js.map +1 -0
  76. package/dist/commands/login.d.ts +5 -0
  77. package/dist/commands/login.d.ts.map +1 -0
  78. package/dist/commands/login.js +91 -0
  79. package/dist/commands/login.js.map +1 -0
  80. package/dist/commands/model.d.ts +14 -0
  81. package/dist/commands/model.d.ts.map +1 -0
  82. package/dist/commands/model.js +110 -0
  83. package/dist/commands/model.js.map +1 -0
  84. package/dist/commands/new.d.ts +5 -0
  85. package/dist/commands/new.d.ts.map +1 -0
  86. package/dist/commands/new.js +24 -0
  87. package/dist/commands/new.js.map +1 -0
  88. package/dist/commands/parse.d.ts +7 -0
  89. package/dist/commands/parse.d.ts.map +1 -0
  90. package/dist/commands/parse.js +17 -0
  91. package/dist/commands/parse.js.map +1 -0
  92. package/dist/commands/registry.d.ts +4 -0
  93. package/dist/commands/registry.d.ts.map +1 -0
  94. package/dist/commands/registry.js +9 -0
  95. package/dist/commands/registry.js.map +1 -0
  96. package/dist/commands/sandbox.d.ts +10 -0
  97. package/dist/commands/sandbox.d.ts.map +1 -0
  98. package/dist/commands/sandbox.js +83 -0
  99. package/dist/commands/sandbox.js.map +1 -0
  100. package/dist/commands/session-view.d.ts +5 -0
  101. package/dist/commands/session-view.d.ts.map +1 -0
  102. package/dist/commands/session-view.js +62 -0
  103. package/dist/commands/session-view.js.map +1 -0
  104. package/dist/commands/types.d.ts +41 -0
  105. package/dist/commands/types.d.ts.map +1 -0
  106. package/dist/commands/types.js +2 -0
  107. package/dist/commands/types.js.map +1 -0
  108. package/dist/commands/utils.d.ts +8 -0
  109. package/dist/commands/utils.d.ts.map +1 -0
  110. package/dist/commands/utils.js +14 -0
  111. package/dist/commands/utils.js.map +1 -0
  112. package/dist/config.d.ts +59 -0
  113. package/dist/config.d.ts.map +1 -0
  114. package/dist/config.js +370 -0
  115. package/dist/config.js.map +1 -0
  116. package/dist/context.d.ts +17 -0
  117. package/dist/context.d.ts.map +1 -0
  118. package/dist/context.js +24 -0
  119. package/dist/context.js.map +1 -0
  120. package/dist/conversation-history.d.ts +16 -0
  121. package/dist/conversation-history.d.ts.map +1 -0
  122. package/dist/conversation-history.js +144 -0
  123. package/dist/conversation-history.js.map +1 -0
  124. package/dist/download.d.ts +2 -0
  125. package/dist/download.d.ts.map +1 -0
  126. package/dist/download.js +89 -0
  127. package/dist/download.js.map +1 -0
  128. package/dist/env.d.ts +3 -0
  129. package/dist/env.d.ts.map +1 -0
  130. package/dist/env.js +12 -0
  131. package/dist/env.js.map +1 -0
  132. package/dist/events.d.ts +85 -0
  133. package/dist/events.d.ts.map +1 -0
  134. package/dist/events.js +483 -0
  135. package/dist/events.js.map +1 -0
  136. package/dist/execution-resolver.d.ts +25 -0
  137. package/dist/execution-resolver.d.ts.map +1 -0
  138. package/dist/execution-resolver.js +167 -0
  139. package/dist/execution-resolver.js.map +1 -0
  140. package/dist/file-guards.d.ts +9 -0
  141. package/dist/file-guards.d.ts.map +1 -0
  142. package/dist/file-guards.js +56 -0
  143. package/dist/file-guards.js.map +1 -0
  144. package/dist/fs-atomic.d.ts +10 -0
  145. package/dist/fs-atomic.d.ts.map +1 -0
  146. package/dist/fs-atomic.js +45 -0
  147. package/dist/fs-atomic.js.map +1 -0
  148. package/dist/index.d.ts +10 -0
  149. package/dist/index.d.ts.map +1 -0
  150. package/dist/index.js +7 -0
  151. package/dist/index.js.map +1 -0
  152. package/dist/instrument.d.ts +2 -0
  153. package/dist/instrument.d.ts.map +1 -0
  154. package/dist/instrument.js +10 -0
  155. package/dist/instrument.js.map +1 -0
  156. package/dist/log.d.ts +36 -0
  157. package/dist/log.d.ts.map +1 -0
  158. package/dist/log.js +206 -0
  159. package/dist/log.js.map +1 -0
  160. package/dist/login/index.d.ts +42 -0
  161. package/dist/login/index.d.ts.map +1 -0
  162. package/dist/login/index.js +239 -0
  163. package/dist/login/index.js.map +1 -0
  164. package/dist/login/portal.d.ts +19 -0
  165. package/dist/login/portal.d.ts.map +1 -0
  166. package/dist/login/portal.js +1544 -0
  167. package/dist/login/portal.js.map +1 -0
  168. package/dist/login/session.d.ts +26 -0
  169. package/dist/login/session.d.ts.map +1 -0
  170. package/dist/login/session.js +56 -0
  171. package/dist/login/session.js.map +1 -0
  172. package/dist/main.d.ts +3 -0
  173. package/dist/main.d.ts.map +1 -0
  174. package/dist/main.js +366 -0
  175. package/dist/main.js.map +1 -0
  176. package/dist/provisioner.d.ts +83 -0
  177. package/dist/provisioner.d.ts.map +1 -0
  178. package/dist/provisioner.js +500 -0
  179. package/dist/provisioner.js.map +1 -0
  180. package/dist/runtime/conversation-orchestrator.d.ts +40 -0
  181. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  182. package/dist/runtime/conversation-orchestrator.js +183 -0
  183. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  184. package/dist/runtime/index.d.ts +2 -0
  185. package/dist/runtime/index.d.ts.map +1 -0
  186. package/dist/runtime/index.js +2 -0
  187. package/dist/runtime/index.js.map +1 -0
  188. package/dist/runtime/session-runtime.d.ts +26 -0
  189. package/dist/runtime/session-runtime.d.ts.map +1 -0
  190. package/dist/runtime/session-runtime.js +221 -0
  191. package/dist/runtime/session-runtime.js.map +1 -0
  192. package/dist/sandbox/cloudflare.d.ts +15 -0
  193. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  194. package/dist/sandbox/cloudflare.js +138 -0
  195. package/dist/sandbox/cloudflare.js.map +1 -0
  196. package/dist/sandbox/container.d.ts +16 -0
  197. package/dist/sandbox/container.d.ts.map +1 -0
  198. package/dist/sandbox/container.js +138 -0
  199. package/dist/sandbox/container.js.map +1 -0
  200. package/dist/sandbox/errors.d.ts +6 -0
  201. package/dist/sandbox/errors.d.ts.map +1 -0
  202. package/dist/sandbox/errors.js +11 -0
  203. package/dist/sandbox/errors.js.map +1 -0
  204. package/dist/sandbox/firecracker.d.ts +17 -0
  205. package/dist/sandbox/firecracker.d.ts.map +1 -0
  206. package/dist/sandbox/firecracker.js +212 -0
  207. package/dist/sandbox/firecracker.js.map +1 -0
  208. package/dist/sandbox/host.d.ts +11 -0
  209. package/dist/sandbox/host.d.ts.map +1 -0
  210. package/dist/sandbox/host.js +89 -0
  211. package/dist/sandbox/host.js.map +1 -0
  212. package/dist/sandbox/image.d.ts +5 -0
  213. package/dist/sandbox/image.d.ts.map +1 -0
  214. package/dist/sandbox/image.js +30 -0
  215. package/dist/sandbox/image.js.map +1 -0
  216. package/dist/sandbox/index.d.ts +22 -0
  217. package/dist/sandbox/index.d.ts.map +1 -0
  218. package/dist/sandbox/index.js +54 -0
  219. package/dist/sandbox/index.js.map +1 -0
  220. package/dist/sandbox/path-context.d.ts +4 -0
  221. package/dist/sandbox/path-context.d.ts.map +1 -0
  222. package/dist/sandbox/path-context.js +20 -0
  223. package/dist/sandbox/path-context.js.map +1 -0
  224. package/dist/sandbox/types.d.ts +67 -0
  225. package/dist/sandbox/types.d.ts.map +1 -0
  226. package/dist/sandbox/types.js +2 -0
  227. package/dist/sandbox/types.js.map +1 -0
  228. package/dist/sandbox/utils.d.ts +4 -0
  229. package/dist/sandbox/utils.d.ts.map +1 -0
  230. package/dist/sandbox/utils.js +51 -0
  231. package/dist/sandbox/utils.js.map +1 -0
  232. package/dist/sentry.d.ts +50 -0
  233. package/dist/sentry.d.ts.map +1 -0
  234. package/dist/sentry.js +257 -0
  235. package/dist/sentry.js.map +1 -0
  236. package/dist/session-view/command.d.ts +5 -0
  237. package/dist/session-view/command.d.ts.map +1 -0
  238. package/dist/session-view/command.js +7 -0
  239. package/dist/session-view/command.js.map +1 -0
  240. package/dist/session-view/portal.d.ts +16 -0
  241. package/dist/session-view/portal.d.ts.map +1 -0
  242. package/dist/session-view/portal.js +1822 -0
  243. package/dist/session-view/portal.js.map +1 -0
  244. package/dist/session-view/service.d.ts +34 -0
  245. package/dist/session-view/service.d.ts.map +1 -0
  246. package/dist/session-view/service.js +434 -0
  247. package/dist/session-view/service.js.map +1 -0
  248. package/dist/session-view/store.d.ts +18 -0
  249. package/dist/session-view/store.d.ts.map +1 -0
  250. package/dist/session-view/store.js +36 -0
  251. package/dist/session-view/store.js.map +1 -0
  252. package/dist/sessions/metadata.d.ts +15 -0
  253. package/dist/sessions/metadata.d.ts.map +1 -0
  254. package/dist/sessions/metadata.js +11 -0
  255. package/dist/sessions/metadata.js.map +1 -0
  256. package/dist/sessions/policy.d.ts +13 -0
  257. package/dist/sessions/policy.d.ts.map +1 -0
  258. package/dist/sessions/policy.js +23 -0
  259. package/dist/sessions/policy.js.map +1 -0
  260. package/dist/sessions/store.d.ts +103 -0
  261. package/dist/sessions/store.d.ts.map +1 -0
  262. package/dist/sessions/store.js +349 -0
  263. package/dist/sessions/store.js.map +1 -0
  264. package/dist/store.d.ts +58 -0
  265. package/dist/store.d.ts.map +1 -0
  266. package/dist/store.js +152 -0
  267. package/dist/store.js.map +1 -0
  268. package/dist/tool-diagnostics.d.ts +2 -0
  269. package/dist/tool-diagnostics.d.ts.map +1 -0
  270. package/dist/tool-diagnostics.js +7 -0
  271. package/dist/tool-diagnostics.js.map +1 -0
  272. package/dist/tools/bash.d.ts +10 -0
  273. package/dist/tools/bash.d.ts.map +1 -0
  274. package/dist/tools/bash.js +80 -0
  275. package/dist/tools/bash.js.map +1 -0
  276. package/dist/tools/edit.d.ts +11 -0
  277. package/dist/tools/edit.d.ts.map +1 -0
  278. package/dist/tools/edit.js +133 -0
  279. package/dist/tools/edit.js.map +1 -0
  280. package/dist/tools/event.d.ts +62 -0
  281. package/dist/tools/event.d.ts.map +1 -0
  282. package/dist/tools/event.js +138 -0
  283. package/dist/tools/event.js.map +1 -0
  284. package/dist/tools/index.d.ts +14 -0
  285. package/dist/tools/index.d.ts.map +1 -0
  286. package/dist/tools/index.js +23 -0
  287. package/dist/tools/index.js.map +1 -0
  288. package/dist/tools/read.d.ts +11 -0
  289. package/dist/tools/read.d.ts.map +1 -0
  290. package/dist/tools/read.js +136 -0
  291. package/dist/tools/read.js.map +1 -0
  292. package/dist/tools/truncate.d.ts +57 -0
  293. package/dist/tools/truncate.d.ts.map +1 -0
  294. package/dist/tools/truncate.js +184 -0
  295. package/dist/tools/truncate.js.map +1 -0
  296. package/dist/tools/write.d.ts +10 -0
  297. package/dist/tools/write.d.ts.map +1 -0
  298. package/dist/tools/write.js +33 -0
  299. package/dist/tools/write.js.map +1 -0
  300. package/dist/trigger.d.ts +31 -0
  301. package/dist/trigger.d.ts.map +1 -0
  302. package/dist/trigger.js +98 -0
  303. package/dist/trigger.js.map +1 -0
  304. package/dist/ui-copy.d.ts +12 -0
  305. package/dist/ui-copy.d.ts.map +1 -0
  306. package/dist/ui-copy.js +36 -0
  307. package/dist/ui-copy.js.map +1 -0
  308. package/dist/vault-routing.d.ts +4 -0
  309. package/dist/vault-routing.d.ts.map +1 -0
  310. package/dist/vault-routing.js +16 -0
  311. package/dist/vault-routing.js.map +1 -0
  312. package/dist/vault.d.ts +72 -0
  313. package/dist/vault.d.ts.map +1 -0
  314. package/dist/vault.js +281 -0
  315. package/dist/vault.js.map +1 -0
  316. package/package.json +83 -0
@@ -0,0 +1,23 @@
1
+ export function resolveChatSessionKey(options) {
2
+ const { conversationId, conversationKind, messageId, persistentTopLevel, scopeDirectThreads, threadTs, } = options;
3
+ if (conversationKind === "direct" && (!threadTs || !scopeDirectThreads)) {
4
+ return conversationId;
5
+ }
6
+ if (!threadTs && persistentTopLevel) {
7
+ return conversationId;
8
+ }
9
+ return `${conversationId}:${threadTs || messageId}`;
10
+ }
11
+ export function inferConversationKind(platform, conversationId) {
12
+ if (platform === "slack") {
13
+ return conversationId.startsWith("D") ? "direct" : "shared";
14
+ }
15
+ if (platform === "telegram") {
16
+ return conversationId.startsWith("-") ? "shared" : "direct";
17
+ }
18
+ if (platform === "discord") {
19
+ return conversationId.startsWith("DM") ? "direct" : "shared";
20
+ }
21
+ return "shared";
22
+ }
23
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/sessions/policy.ts"],"names":[],"mappings":"AAaA,MAAM,UAAU,qBAAqB,CAAC,OAAiC;IACrE,MAAM,EACJ,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,kBAAkB,EAClB,QAAQ,GACT,GAAG,OAAO,CAAC;IACZ,IAAI,gBAAgB,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACxE,OAAO,cAAc,CAAC;IACxB,CAAC;IACD,IAAI,CAAC,QAAQ,IAAI,kBAAkB,EAAE,CAAC;QACpC,OAAO,cAAc,CAAC;IACxB,CAAC;IACD,OAAO,GAAG,cAAc,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,QAAsB,EACtB,cAAsB;IAEtB,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC9D,CAAC;IAED,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC5B,OAAO,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC9D,CAAC;IAED,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,cAAc,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC/D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["import type { ConversationKind } from \"../adapter.js\";\n\nexport type ChatPlatform = \"slack\" | \"telegram\" | \"discord\" | string;\n\nexport interface ResolveSessionKeyOptions {\n conversationId: string;\n conversationKind: ConversationKind;\n messageId: string;\n threadTs?: string;\n persistentTopLevel?: boolean;\n scopeDirectThreads?: boolean;\n}\n\nexport function resolveChatSessionKey(options: ResolveSessionKeyOptions): string {\n const {\n conversationId,\n conversationKind,\n messageId,\n persistentTopLevel,\n scopeDirectThreads,\n threadTs,\n } = options;\n if (conversationKind === \"direct\" && (!threadTs || !scopeDirectThreads)) {\n return conversationId;\n }\n if (!threadTs && persistentTopLevel) {\n return conversationId;\n }\n return `${conversationId}:${threadTs || messageId}`;\n}\n\nexport function inferConversationKind(\n platform: ChatPlatform,\n conversationId: string,\n): ConversationKind {\n if (platform === \"slack\") {\n return conversationId.startsWith(\"D\") ? \"direct\" : \"shared\";\n }\n\n if (platform === \"telegram\") {\n return conversationId.startsWith(\"-\") ? \"shared\" : \"direct\";\n }\n\n if (platform === \"discord\") {\n return conversationId.startsWith(\"DM\") ? \"direct\" : \"shared\";\n }\n\n return \"shared\";\n}\n"]}
@@ -0,0 +1,103 @@
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ export declare class ThreadRootNotFoundError extends Error {
3
+ constructor(sessionFile: string);
4
+ }
5
+ export interface ThreadRootMessage {
6
+ text?: string;
7
+ userName?: string;
8
+ user?: string;
9
+ loggedAt?: number;
10
+ }
11
+ export interface ResolvedSessionScope {
12
+ sessionDir: string;
13
+ contextFile: string;
14
+ threadRootMessage: ThreadRootMessage | null;
15
+ }
16
+ export interface ResolveGenericSessionScopeOptions {
17
+ conversationDir: string;
18
+ sessionKey: string;
19
+ cwd?: string;
20
+ }
21
+ /**
22
+ * Returns the shared session directory for a conversation.
23
+ * Channel sessions use a current pointer within this directory.
24
+ * Thread sessions are stored as fixed files within the same directory.
25
+ */
26
+ export declare function getChannelSessionDir(channelDir: string): string;
27
+ /**
28
+ * Resolves the current active session file for a session directory.
29
+ * Reads the "current" pointer file; creates a new session if none exists
30
+ * or the pointed-to file is missing.
31
+ */
32
+ export declare function resolveSessionFile(sessionDir: string): string;
33
+ /**
34
+ * Resolve the current active session file for a session directory.
35
+ * Creates a fully initialized persistent session with the provided cwd when none exists.
36
+ */
37
+ export declare function resolveManagedSessionFile(sessionDir: string, cwd: string): string;
38
+ /**
39
+ * Extracts the short UUID from a session file path.
40
+ * e.g. "2026-04-05T00-00_7b54cf90.jsonl" → "7b54cf90"
41
+ */
42
+ export declare function extractSessionUuid(sessionFile: string): string;
43
+ /**
44
+ * Extracts the thread/suffix part of a session key.
45
+ * "channelId:threadId" → "threadId", "channelId" → "channelId"
46
+ */
47
+ export declare function extractSessionSuffix(sessionKey: string): string;
48
+ /**
49
+ * Creates an empty timestamped file and updates the "current" pointer.
50
+ * Used only by tests for placeholder-file scenarios.
51
+ *
52
+ * Order matters: write the session file first, then atomic-rename the pointer
53
+ * last so a crash mid-create never leaves "current" pointing at a missing file.
54
+ */
55
+ export declare function createNewSessionFile(sessionDir: string): string;
56
+ /**
57
+ * Creates a new persistent session file with a proper SessionManager header and cwd.
58
+ * Also updates the "current" pointer. Header is written before the pointer flips so a
59
+ * partial create cannot leave "current" pointing at a missing file.
60
+ */
61
+ export declare function createManagedSessionFile(sessionDir: string, cwd: string): string;
62
+ /**
63
+ * Open a session file with an explicit cwd, even if the file does not exist yet.
64
+ * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.
65
+ */
66
+ export declare function openManagedSession(sessionFile: string, sessionDir: string, cwd: string): SessionManager;
67
+ /**
68
+ * Creates or overwrites a fixed-path session file with a valid session header.
69
+ */
70
+ export declare function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string;
71
+ /**
72
+ * Returns the fixed session file path for a Slack thread.
73
+ */
74
+ export declare function getThreadSessionFile(channelDir: string, sessionKey: string): string;
75
+ /**
76
+ * Resolve the default session scope for platforms without Slack-style branch forking.
77
+ * Top-level/private sessions use the conversation's current pointer. Threaded or
78
+ * per-message sessions use a fixed file derived from the session key suffix.
79
+ */
80
+ export declare function resolveGenericSessionScope(options: ResolveGenericSessionScopeOptions): ResolvedSessionScope;
81
+ /**
82
+ * Try to resolve an existing current session file.
83
+ * Returns null if no current pointer exists or the pointed file has no valid session header.
84
+ */
85
+ export declare function tryResolveCurrentSession(sessionDir: string): string | null;
86
+ /**
87
+ * Try to resolve an existing thread session file.
88
+ * Returns the file path if found, or null if no valid thread session exists yet.
89
+ */
90
+ export declare function tryResolveThreadSession(sessionFile: string): string | null;
91
+ /**
92
+ * Resolve the channel's current session file path (for fork source).
93
+ * Returns null if no channel session exists.
94
+ */
95
+ export declare function resolveChannelSessionFile(channelDir: string): string | null;
96
+ /**
97
+ * Fork a channel session into a fixed thread-session path.
98
+ * The resulting file keeps forkFrom's distinct session/header metadata.
99
+ */
100
+ export declare function forkThreadSessionFile(sourceSessionFile: string, targetSessionFile: string, cwd: string): string;
101
+ export declare function createThreadSessionFileFromRootMessage(targetSessionFile: string, cwd: string, rootMessage: ThreadRootMessage, parentSession?: string): string;
102
+ export declare function forkThreadSessionFileFromRootMessage(sourceSessionFile: string, targetSessionFile: string, cwd: string, rootMessage: ThreadRootMessage): string;
103
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sessions/store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAKjE,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,YAAY,WAAW,EAAE,MAAM,EAG9B;CACF;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAcD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,cAAc,CAShB;AAQD;;GAEG;AACH,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvF;AAeD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,oBAAoB,CAoBtB;AAuID;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI1E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE1E;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE3E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,GACV,MAAM,CAWR;AAED,wBAAgB,sCAAsC,CACpD,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,EAC9B,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAiCR;AAED,wBAAgB,oCAAoC,CAClD,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,GAC7B,MAAM,CAqBR","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { existsSync, mkdirSync, renameSync, rmSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"../file-guards.js\";\nimport { atomicWritePrivateFile } from \"../fs-atomic.js\";\nimport { isPlatformHistorySession } from \"./metadata.js\";\n\nexport class ThreadRootNotFoundError extends Error {\n constructor(sessionFile: string) {\n super(`Thread root message not found in source session: ${sessionFile}`);\n this.name = \"ThreadRootNotFoundError\";\n }\n}\n\nexport interface ThreadRootMessage {\n text?: string;\n userName?: string;\n user?: string;\n loggedAt?: number;\n}\n\nexport interface ResolvedSessionScope {\n sessionDir: string;\n contextFile: string;\n threadRootMessage: ThreadRootMessage | null;\n}\n\nexport interface ResolveGenericSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\ninterface SessionMessageEntryLike {\n type: string;\n id: string;\n parentId: string | null;\n timestamp: string;\n message?: {\n role?: string;\n timestamp?: number;\n content?: Array<{ type?: string; text?: string }> | string;\n };\n}\n\n/**\n * Returns the shared session directory for a conversation.\n * Channel sessions use a current pointer within this directory.\n * Thread sessions are stored as fixed files within the same directory.\n */\nexport function getChannelSessionDir(channelDir: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Resolves the current active session file for a session directory.\n * Reads the \"current\" pointer file; creates a new session if none exists\n * or the pointed-to file is missing.\n */\nexport function resolveSessionFile(sessionDir: string): string {\n const existing = tryResolveCurrentSession(sessionDir);\n if (existing) return existing;\n return createNewSessionFile(sessionDir);\n}\n\n/**\n * Resolve the current active session file for a session directory.\n * Creates a fully initialized persistent session with the provided cwd when none exists.\n */\nexport function resolveManagedSessionFile(sessionDir: string, cwd: string): string {\n const existingPath = getCurrentSessionPath(sessionDir);\n if (existingPath && !isPlatformHistorySession(existingPath)) return existingPath;\n return createManagedSessionFile(sessionDir, cwd);\n}\n\n/**\n * Extracts the short UUID from a session file path.\n * e.g. \"2026-04-05T00-00_7b54cf90.jsonl\" → \"7b54cf90\"\n */\nexport function extractSessionUuid(sessionFile: string): string {\n const base = sessionFile.split(\"/\").pop() ?? sessionFile;\n return base.replace(\".jsonl\", \"\").split(\"_\").pop() ?? base;\n}\n\n/**\n * Extracts the thread/suffix part of a session key.\n * \"channelId:threadId\" → \"threadId\", \"channelId\" → \"channelId\"\n */\nexport function extractSessionSuffix(sessionKey: string): string {\n const parts = sessionKey.split(\":\");\n return parts.length > 1 ? parts[parts.length - 1] : sessionKey;\n}\n\n/**\n * Creates an empty timestamped file and updates the \"current\" pointer.\n * Used only by tests for placeholder-file scenarios.\n *\n * Order matters: write the session file first, then atomic-rename the pointer\n * last so a crash mid-create never leaves \"current\" pointing at a missing file.\n */\nexport function createNewSessionFile(sessionDir: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const uuid = randomUUID().slice(0, 8);\n const filename = `${timestamp}_${uuid}.jsonl`;\n const filePath = join(sessionDir, filename);\n atomicWritePrivateFile(filePath, \"\");\n atomicWritePrivateFile(join(sessionDir, \"current\"), filename);\n return filePath;\n}\n\n/**\n * Creates a new persistent session file with a proper SessionManager header and cwd.\n * Also updates the \"current\" pointer. Header is written before the pointer flips so a\n * partial create cannot leave \"current\" pointing at a missing file.\n */\nexport function createManagedSessionFile(sessionDir: string, cwd: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const sessionId = randomUUID();\n const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);\n writeSessionHeader(sessionFile, cwd, sessionId);\n setCurrentPointer(sessionDir, sessionFile);\n return sessionFile;\n}\n\n/**\n * Open a session file with an explicit cwd, even if the file does not exist yet.\n * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.\n */\nexport function openManagedSession(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n): SessionManager {\n if (shouldRecreatePreinitializedSession(sessionFile)) {\n rmSync(sessionFile, { force: true });\n }\n\n const SessionManagerCtor = SessionManager as unknown as {\n new (cwd: string, sessionDir: string, sessionFile: string, persist: boolean): SessionManager;\n };\n return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);\n}\n\nfunction setCurrentPointer(sessionDir: string, sessionFilePath: string): void {\n const filename = sessionFilePath.split(\"/\").pop()!;\n mkdirSync(sessionDir, { recursive: true });\n atomicWritePrivateFile(join(sessionDir, \"current\"), filename);\n}\n\n/**\n * Creates or overwrites a fixed-path session file with a valid session header.\n */\nexport function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string {\n writeSessionHeader(sessionFile, cwd);\n return sessionFile;\n}\n\nfunction writeSessionHeader(sessionFile: string, cwd: string, sessionId = randomUUID()): void {\n const sessionDir = getFileDir(sessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: new Date().toISOString(),\n cwd,\n };\n atomicWritePrivateFile(sessionFile, `${JSON.stringify(header)}\\n`);\n}\n\n/**\n * Returns the fixed session file path for a Slack thread.\n */\nexport function getThreadSessionFile(channelDir: string, sessionKey: string): string {\n return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);\n}\n\n/**\n * Resolve the default session scope for platforms without Slack-style branch forking.\n * Top-level/private sessions use the conversation's current pointer. Threaded or\n * per-message sessions use a fixed file derived from the session key suffix.\n */\nexport function resolveGenericSessionScope(\n options: ResolveGenericSessionScopeOptions,\n): ResolvedSessionScope {\n const { conversationDir, sessionKey } = options;\n const cwd = options.cwd ?? conversationDir;\n const sessionDir = getChannelSessionDir(conversationDir);\n\n if (!sessionKey.includes(\":\")) {\n return {\n sessionDir,\n contextFile: resolveManagedSessionFile(sessionDir, cwd),\n threadRootMessage: null,\n };\n }\n\n const threadFile = getThreadSessionFile(conversationDir, sessionKey);\n return {\n sessionDir,\n contextFile:\n tryResolveThreadSession(threadFile) ?? createManagedSessionFileAtPath(threadFile, cwd),\n threadRootMessage: null,\n };\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const raw = readTextFileIfExists(sessionFile);\n if (raw === undefined) return false;\n const lines = raw.split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = parseJsonValue(\n trimmed,\n (value): value is { type?: string } => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction shouldRecreatePreinitializedSession(sessionFile: string): boolean {\n try {\n const raw = readTextFileIfExists(sessionFile);\n if (raw === undefined) return false;\n const entries = raw\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) =>\n parseJsonValue(\n line,\n (value): value is { type?: string } => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n ),\n );\n\n return entries.length === 1 && entries[0]?.type === \"session\";\n } catch {\n return false;\n }\n}\n\nfunction getFileDir(sessionFile: string): string {\n return sessionFile.substring(0, sessionFile.lastIndexOf(\"/\"));\n}\n\nfunction resolveThreadSnapshotEntries(\n sourceSessionFile: string,\n rootMessage: ThreadRootMessage,\n): SessionMessageEntryLike[] | null {\n const targetText = buildComparableRootMessageText(rootMessage);\n if (!targetText) return null;\n\n const entries = SessionManager.open(sourceSessionFile).getEntries() as SessionMessageEntryLike[];\n const matchIndex = findRootMessageIndex(entries, targetText, rootMessage.loggedAt);\n if (matchIndex === -1) return null;\n\n const nextTopLevelUserIndex = entries.findIndex(\n (entry, index) => index > matchIndex && isUserMessageEntry(entry),\n );\n const endIndex = nextTopLevelUserIndex === -1 ? entries.length : nextTopLevelUserIndex;\n return entries.slice(0, endIndex);\n}\n\nfunction findRootMessageIndex(\n entries: SessionMessageEntryLike[],\n targetText: string,\n loggedAt?: number,\n): number {\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (!isUserMessageEntry(entry)) continue;\n\n const comparableText = normalizeComparableUserText(getMessageText(entry));\n if (comparableText !== targetText) continue;\n\n const messageTimestamp = entry.message?.timestamp;\n if (\n loggedAt !== undefined &&\n typeof messageTimestamp === \"number\" &&\n messageTimestamp < loggedAt\n ) {\n continue;\n }\n\n return i;\n }\n\n return -1;\n}\n\nfunction isUserMessageEntry(entry: SessionMessageEntryLike): boolean {\n return entry.type === \"message\" && entry.message?.role === \"user\";\n}\n\nfunction getMessageText(entry: SessionMessageEntryLike): string {\n const content = entry.message?.content;\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n return content\n .filter((part): part is { type?: string; text?: string } => part.type === \"text\")\n .map((part) => part.text ?? \"\")\n .join(\"\\n\\n\");\n}\n\nfunction buildComparableRootMessageText(rootMessage: ThreadRootMessage): string | null {\n const userLabel = rootMessage.userName || rootMessage.user || \"unknown\";\n const text = rootMessage.text?.trim();\n if (!text) return null;\n return normalizeComparableUserText(`[${userLabel}]: ${text}`);\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction getCurrentSessionPath(sessionDir: string): string | null {\n const pointerFile = join(sessionDir, \"current\");\n const filename = readTextFileIfExists(pointerFile)?.trim();\n if (!filename) return null;\n return join(sessionDir, filename);\n}\n\n/**\n * Try to resolve an existing current session file.\n * Returns null if no current pointer exists or the pointed file has no valid session header.\n */\nexport function tryResolveCurrentSession(sessionDir: string): string | null {\n const fullPath = getCurrentSessionPath(sessionDir);\n if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath)) return fullPath;\n return null;\n}\n\n/**\n * Try to resolve an existing thread session file.\n * Returns the file path if found, or null if no valid thread session exists yet.\n */\nexport function tryResolveThreadSession(sessionFile: string): string | null {\n return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;\n}\n\n/**\n * Resolve the channel's current session file path (for fork source).\n * Returns null if no channel session exists.\n */\nexport function resolveChannelSessionFile(channelDir: string): string | null {\n return tryResolveCurrentSession(getChannelSessionDir(channelDir));\n}\n\n/**\n * Fork a channel session into a fixed thread-session path.\n * The resulting file keeps forkFrom's distinct session/header metadata.\n */\nexport function forkThreadSessionFile(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);\n const forkedFile = forked.getSessionFile();\n if (!forkedFile) {\n throw new Error(`Failed to fork session from ${sourceSessionFile}`);\n }\n rmSync(targetSessionFile, { force: true });\n renameSync(forkedFile, targetSessionFile);\n return targetSessionFile;\n}\n\nexport function createThreadSessionFileFromRootMessage(\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n parentSession?: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n ...(parentSession ? { parentSession } : {}),\n };\n const rootText = buildComparableRootMessageText(rootMessage);\n if (!rootText) {\n atomicWritePrivateFile(targetSessionFile, `${JSON.stringify(header)}\\n`);\n return targetSessionFile;\n }\n\n const rootEntry = {\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date().toISOString(),\n message: {\n role: \"user\",\n content: [{ type: \"text\", text: rootText }],\n ...(rootMessage.loggedAt !== undefined ? { timestamp: rootMessage.loggedAt } : {}),\n },\n };\n const content = [header, rootEntry].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(targetSessionFile, `${content}\\n`);\n return targetSessionFile;\n}\n\nexport function forkThreadSessionFileFromRootMessage(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n): string {\n const snapshotEntries = resolveThreadSnapshotEntries(sourceSessionFile, rootMessage);\n if (!snapshotEntries) {\n throw new ThreadRootNotFoundError(sourceSessionFile);\n }\n\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n parentSession: sourceSessionFile,\n };\n const content = [header, ...snapshotEntries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(targetSessionFile, `${content}\\n`);\n return targetSessionFile;\n}\n"]}
@@ -0,0 +1,349 @@
1
+ import { randomUUID } from "crypto";
2
+ import { existsSync, mkdirSync, renameSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
5
+ import { isRecord, parseJsonValue, readTextFileIfExists } from "../file-guards.js";
6
+ import { atomicWritePrivateFile } from "../fs-atomic.js";
7
+ import { isPlatformHistorySession } from "./metadata.js";
8
+ export class ThreadRootNotFoundError extends Error {
9
+ constructor(sessionFile) {
10
+ super(`Thread root message not found in source session: ${sessionFile}`);
11
+ this.name = "ThreadRootNotFoundError";
12
+ }
13
+ }
14
+ /**
15
+ * Returns the shared session directory for a conversation.
16
+ * Channel sessions use a current pointer within this directory.
17
+ * Thread sessions are stored as fixed files within the same directory.
18
+ */
19
+ export function getChannelSessionDir(channelDir) {
20
+ return join(channelDir, "sessions");
21
+ }
22
+ /**
23
+ * Resolves the current active session file for a session directory.
24
+ * Reads the "current" pointer file; creates a new session if none exists
25
+ * or the pointed-to file is missing.
26
+ */
27
+ export function resolveSessionFile(sessionDir) {
28
+ const existing = tryResolveCurrentSession(sessionDir);
29
+ if (existing)
30
+ return existing;
31
+ return createNewSessionFile(sessionDir);
32
+ }
33
+ /**
34
+ * Resolve the current active session file for a session directory.
35
+ * Creates a fully initialized persistent session with the provided cwd when none exists.
36
+ */
37
+ export function resolveManagedSessionFile(sessionDir, cwd) {
38
+ const existingPath = getCurrentSessionPath(sessionDir);
39
+ if (existingPath && !isPlatformHistorySession(existingPath))
40
+ return existingPath;
41
+ return createManagedSessionFile(sessionDir, cwd);
42
+ }
43
+ /**
44
+ * Extracts the short UUID from a session file path.
45
+ * e.g. "2026-04-05T00-00_7b54cf90.jsonl" → "7b54cf90"
46
+ */
47
+ export function extractSessionUuid(sessionFile) {
48
+ const base = sessionFile.split("/").pop() ?? sessionFile;
49
+ return base.replace(".jsonl", "").split("_").pop() ?? base;
50
+ }
51
+ /**
52
+ * Extracts the thread/suffix part of a session key.
53
+ * "channelId:threadId" → "threadId", "channelId" → "channelId"
54
+ */
55
+ export function extractSessionSuffix(sessionKey) {
56
+ const parts = sessionKey.split(":");
57
+ return parts.length > 1 ? parts[parts.length - 1] : sessionKey;
58
+ }
59
+ /**
60
+ * Creates an empty timestamped file and updates the "current" pointer.
61
+ * Used only by tests for placeholder-file scenarios.
62
+ *
63
+ * Order matters: write the session file first, then atomic-rename the pointer
64
+ * last so a crash mid-create never leaves "current" pointing at a missing file.
65
+ */
66
+ export function createNewSessionFile(sessionDir) {
67
+ mkdirSync(sessionDir, { recursive: true });
68
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
69
+ const uuid = randomUUID().slice(0, 8);
70
+ const filename = `${timestamp}_${uuid}.jsonl`;
71
+ const filePath = join(sessionDir, filename);
72
+ atomicWritePrivateFile(filePath, "");
73
+ atomicWritePrivateFile(join(sessionDir, "current"), filename);
74
+ return filePath;
75
+ }
76
+ /**
77
+ * Creates a new persistent session file with a proper SessionManager header and cwd.
78
+ * Also updates the "current" pointer. Header is written before the pointer flips so a
79
+ * partial create cannot leave "current" pointing at a missing file.
80
+ */
81
+ export function createManagedSessionFile(sessionDir, cwd) {
82
+ mkdirSync(sessionDir, { recursive: true });
83
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
84
+ const sessionId = randomUUID();
85
+ const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);
86
+ writeSessionHeader(sessionFile, cwd, sessionId);
87
+ setCurrentPointer(sessionDir, sessionFile);
88
+ return sessionFile;
89
+ }
90
+ /**
91
+ * Open a session file with an explicit cwd, even if the file does not exist yet.
92
+ * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.
93
+ */
94
+ export function openManagedSession(sessionFile, sessionDir, cwd) {
95
+ if (shouldRecreatePreinitializedSession(sessionFile)) {
96
+ rmSync(sessionFile, { force: true });
97
+ }
98
+ const SessionManagerCtor = SessionManager;
99
+ return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);
100
+ }
101
+ function setCurrentPointer(sessionDir, sessionFilePath) {
102
+ const filename = sessionFilePath.split("/").pop();
103
+ mkdirSync(sessionDir, { recursive: true });
104
+ atomicWritePrivateFile(join(sessionDir, "current"), filename);
105
+ }
106
+ /**
107
+ * Creates or overwrites a fixed-path session file with a valid session header.
108
+ */
109
+ export function createManagedSessionFileAtPath(sessionFile, cwd) {
110
+ writeSessionHeader(sessionFile, cwd);
111
+ return sessionFile;
112
+ }
113
+ function writeSessionHeader(sessionFile, cwd, sessionId = randomUUID()) {
114
+ const sessionDir = getFileDir(sessionFile);
115
+ mkdirSync(sessionDir, { recursive: true });
116
+ const header = {
117
+ type: "session",
118
+ version: 3,
119
+ id: sessionId,
120
+ timestamp: new Date().toISOString(),
121
+ cwd,
122
+ };
123
+ atomicWritePrivateFile(sessionFile, `${JSON.stringify(header)}\n`);
124
+ }
125
+ /**
126
+ * Returns the fixed session file path for a Slack thread.
127
+ */
128
+ export function getThreadSessionFile(channelDir, sessionKey) {
129
+ return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);
130
+ }
131
+ /**
132
+ * Resolve the default session scope for platforms without Slack-style branch forking.
133
+ * Top-level/private sessions use the conversation's current pointer. Threaded or
134
+ * per-message sessions use a fixed file derived from the session key suffix.
135
+ */
136
+ export function resolveGenericSessionScope(options) {
137
+ const { conversationDir, sessionKey } = options;
138
+ const cwd = options.cwd ?? conversationDir;
139
+ const sessionDir = getChannelSessionDir(conversationDir);
140
+ if (!sessionKey.includes(":")) {
141
+ return {
142
+ sessionDir,
143
+ contextFile: resolveManagedSessionFile(sessionDir, cwd),
144
+ threadRootMessage: null,
145
+ };
146
+ }
147
+ const threadFile = getThreadSessionFile(conversationDir, sessionKey);
148
+ return {
149
+ sessionDir,
150
+ contextFile: tryResolveThreadSession(threadFile) ?? createManagedSessionFileAtPath(threadFile, cwd),
151
+ threadRootMessage: null,
152
+ };
153
+ }
154
+ function hasSessionHeader(sessionFile) {
155
+ try {
156
+ const raw = readTextFileIfExists(sessionFile);
157
+ if (raw === undefined)
158
+ return false;
159
+ const lines = raw.split("\n");
160
+ for (const line of lines) {
161
+ const trimmed = line.trim();
162
+ if (!trimmed)
163
+ continue;
164
+ const entry = parseJsonValue(trimmed, (value) => isRecord(value), (detail) => (detail === "unexpected JSON shape" ? "expected a JSON object" : detail));
165
+ return entry.type === "session";
166
+ }
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ return false;
172
+ }
173
+ function shouldRecreatePreinitializedSession(sessionFile) {
174
+ try {
175
+ const raw = readTextFileIfExists(sessionFile);
176
+ if (raw === undefined)
177
+ return false;
178
+ const entries = raw
179
+ .split("\n")
180
+ .map((line) => line.trim())
181
+ .filter(Boolean)
182
+ .map((line) => parseJsonValue(line, (value) => isRecord(value), (detail) => (detail === "unexpected JSON shape" ? "expected a JSON object" : detail)));
183
+ return entries.length === 1 && entries[0]?.type === "session";
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ }
189
+ function getFileDir(sessionFile) {
190
+ return sessionFile.substring(0, sessionFile.lastIndexOf("/"));
191
+ }
192
+ function resolveThreadSnapshotEntries(sourceSessionFile, rootMessage) {
193
+ const targetText = buildComparableRootMessageText(rootMessage);
194
+ if (!targetText)
195
+ return null;
196
+ const entries = SessionManager.open(sourceSessionFile).getEntries();
197
+ const matchIndex = findRootMessageIndex(entries, targetText, rootMessage.loggedAt);
198
+ if (matchIndex === -1)
199
+ return null;
200
+ const nextTopLevelUserIndex = entries.findIndex((entry, index) => index > matchIndex && isUserMessageEntry(entry));
201
+ const endIndex = nextTopLevelUserIndex === -1 ? entries.length : nextTopLevelUserIndex;
202
+ return entries.slice(0, endIndex);
203
+ }
204
+ function findRootMessageIndex(entries, targetText, loggedAt) {
205
+ for (let i = 0; i < entries.length; i++) {
206
+ const entry = entries[i];
207
+ if (!isUserMessageEntry(entry))
208
+ continue;
209
+ const comparableText = normalizeComparableUserText(getMessageText(entry));
210
+ if (comparableText !== targetText)
211
+ continue;
212
+ const messageTimestamp = entry.message?.timestamp;
213
+ if (loggedAt !== undefined &&
214
+ typeof messageTimestamp === "number" &&
215
+ messageTimestamp < loggedAt) {
216
+ continue;
217
+ }
218
+ return i;
219
+ }
220
+ return -1;
221
+ }
222
+ function isUserMessageEntry(entry) {
223
+ return entry.type === "message" && entry.message?.role === "user";
224
+ }
225
+ function getMessageText(entry) {
226
+ const content = entry.message?.content;
227
+ if (typeof content === "string")
228
+ return content;
229
+ if (!Array.isArray(content))
230
+ return "";
231
+ return content
232
+ .filter((part) => part.type === "text")
233
+ .map((part) => part.text ?? "")
234
+ .join("\n\n");
235
+ }
236
+ function buildComparableRootMessageText(rootMessage) {
237
+ const userLabel = rootMessage.userName || rootMessage.user || "unknown";
238
+ const text = rootMessage.text?.trim();
239
+ if (!text)
240
+ return null;
241
+ return normalizeComparableUserText(`[${userLabel}]: ${text}`);
242
+ }
243
+ function stripSlackAttachmentBlock(text) {
244
+ return text.replace(/\n*<slack_attachments>\n[\s\S]*?\n<\/slack_attachments>\s*$/g, "");
245
+ }
246
+ function normalizeComparableUserText(text) {
247
+ const withoutTimestamp = text.replace(/^\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\]\s+(?=\[[^\]]+\](?:\s+\[in-thread:[^\]]+\])?:\s)/, "");
248
+ return stripSlackAttachmentBlock(withoutTimestamp).trim();
249
+ }
250
+ function getCurrentSessionPath(sessionDir) {
251
+ const pointerFile = join(sessionDir, "current");
252
+ const filename = readTextFileIfExists(pointerFile)?.trim();
253
+ if (!filename)
254
+ return null;
255
+ return join(sessionDir, filename);
256
+ }
257
+ /**
258
+ * Try to resolve an existing current session file.
259
+ * Returns null if no current pointer exists or the pointed file has no valid session header.
260
+ */
261
+ export function tryResolveCurrentSession(sessionDir) {
262
+ const fullPath = getCurrentSessionPath(sessionDir);
263
+ if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath))
264
+ return fullPath;
265
+ return null;
266
+ }
267
+ /**
268
+ * Try to resolve an existing thread session file.
269
+ * Returns the file path if found, or null if no valid thread session exists yet.
270
+ */
271
+ export function tryResolveThreadSession(sessionFile) {
272
+ return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;
273
+ }
274
+ /**
275
+ * Resolve the channel's current session file path (for fork source).
276
+ * Returns null if no channel session exists.
277
+ */
278
+ export function resolveChannelSessionFile(channelDir) {
279
+ return tryResolveCurrentSession(getChannelSessionDir(channelDir));
280
+ }
281
+ /**
282
+ * Fork a channel session into a fixed thread-session path.
283
+ * The resulting file keeps forkFrom's distinct session/header metadata.
284
+ */
285
+ export function forkThreadSessionFile(sourceSessionFile, targetSessionFile, cwd) {
286
+ const sessionDir = getFileDir(targetSessionFile);
287
+ mkdirSync(sessionDir, { recursive: true });
288
+ const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);
289
+ const forkedFile = forked.getSessionFile();
290
+ if (!forkedFile) {
291
+ throw new Error(`Failed to fork session from ${sourceSessionFile}`);
292
+ }
293
+ rmSync(targetSessionFile, { force: true });
294
+ renameSync(forkedFile, targetSessionFile);
295
+ return targetSessionFile;
296
+ }
297
+ export function createThreadSessionFileFromRootMessage(targetSessionFile, cwd, rootMessage, parentSession) {
298
+ const sessionDir = getFileDir(targetSessionFile);
299
+ mkdirSync(sessionDir, { recursive: true });
300
+ rmSync(targetSessionFile, { force: true });
301
+ const header = {
302
+ type: "session",
303
+ version: 3,
304
+ id: randomUUID(),
305
+ timestamp: new Date().toISOString(),
306
+ cwd,
307
+ ...(parentSession ? { parentSession } : {}),
308
+ };
309
+ const rootText = buildComparableRootMessageText(rootMessage);
310
+ if (!rootText) {
311
+ atomicWritePrivateFile(targetSessionFile, `${JSON.stringify(header)}\n`);
312
+ return targetSessionFile;
313
+ }
314
+ const rootEntry = {
315
+ type: "message",
316
+ id: randomUUID().slice(0, 8),
317
+ parentId: null,
318
+ timestamp: new Date().toISOString(),
319
+ message: {
320
+ role: "user",
321
+ content: [{ type: "text", text: rootText }],
322
+ ...(rootMessage.loggedAt !== undefined ? { timestamp: rootMessage.loggedAt } : {}),
323
+ },
324
+ };
325
+ const content = [header, rootEntry].map((entry) => JSON.stringify(entry)).join("\n");
326
+ atomicWritePrivateFile(targetSessionFile, `${content}\n`);
327
+ return targetSessionFile;
328
+ }
329
+ export function forkThreadSessionFileFromRootMessage(sourceSessionFile, targetSessionFile, cwd, rootMessage) {
330
+ const snapshotEntries = resolveThreadSnapshotEntries(sourceSessionFile, rootMessage);
331
+ if (!snapshotEntries) {
332
+ throw new ThreadRootNotFoundError(sourceSessionFile);
333
+ }
334
+ const sessionDir = getFileDir(targetSessionFile);
335
+ mkdirSync(sessionDir, { recursive: true });
336
+ rmSync(targetSessionFile, { force: true });
337
+ const header = {
338
+ type: "session",
339
+ version: 3,
340
+ id: randomUUID(),
341
+ timestamp: new Date().toISOString(),
342
+ cwd,
343
+ parentSession: sourceSessionFile,
344
+ };
345
+ const content = [header, ...snapshotEntries].map((entry) => JSON.stringify(entry)).join("\n");
346
+ atomicWritePrivateFile(targetSessionFile, `${content}\n`);
347
+ return targetSessionFile;
348
+ }
349
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/sessions/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YAAY,WAAmB;QAC7B,KAAK,CAAC,oDAAoD,WAAW,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAiCD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,OAAO,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAkB;IACnD,MAAM,QAAQ,GAAG,wBAAwB,CAAC,UAAU,CAAC,CAAC;IACtD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,OAAO,oBAAoB,CAAC,UAAU,CAAC,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,UAAkB,EAAE,GAAW;IACvE,MAAM,YAAY,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC;IACvD,IAAI,YAAY,IAAI,CAAC,wBAAwB,CAAC,YAAY,CAAC;QAAE,OAAO,YAAY,CAAC;IACjF,OAAO,wBAAwB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB;IACpD,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,WAAW,CAAC;IACzD,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,GAAG,SAAS,IAAI,IAAI,QAAQ,CAAC;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC5C,sBAAsB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACrC,sBAAsB,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC9D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,UAAkB,EAAE,GAAW;IACtE,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,UAAU,EAAE,CAAC;IAC/B,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,SAAS,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;IACpF,kBAAkB,CAAC,WAAW,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC;IAChD,iBAAiB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAC3C,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAAmB,EACnB,UAAkB,EAClB,GAAW;IAEX,IAAI,mCAAmC,CAAC,WAAW,CAAC,EAAE,CAAC;QACrD,MAAM,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,kBAAkB,GAAG,cAE1B,CAAC;IACF,OAAO,IAAI,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,iBAAiB,CAAC,UAAkB,EAAE,eAAuB;IACpE,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC;IACnD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,sBAAsB,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,QAAQ,CAAC,CAAC;AAChE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,8BAA8B,CAAC,WAAmB,EAAE,GAAW;IAC7E,kBAAkB,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IACrC,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,SAAS,kBAAkB,CAAC,WAAmB,EAAE,GAAW,EAAE,SAAS,GAAG,UAAU,EAAE;IACpF,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IAC3C,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG;QACb,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,CAAC;QACV,EAAE,EAAE,SAAS;QACb,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,GAAG;KACJ,CAAC;IACF,sBAAsB,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACrE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB,EAAE,UAAkB;IACzE,OAAO,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,EAAE,GAAG,oBAAoB,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AAC7F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CACxC,OAA0C;IAE1C,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,eAAe,CAAC;IAC3C,MAAM,UAAU,GAAG,oBAAoB,CAAC,eAAe,CAAC,CAAC;IAEzD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,UAAU;YACV,WAAW,EAAE,yBAAyB,CAAC,UAAU,EAAE,GAAG,CAAC;YACvD,iBAAiB,EAAE,IAAI;SACxB,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,oBAAoB,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IACrE,OAAO;QACL,UAAU;QACV,WAAW,EACT,uBAAuB,CAAC,UAAU,CAAC,IAAI,8BAA8B,CAAC,UAAU,EAAE,GAAG,CAAC;QACxF,iBAAiB,EAAE,IAAI;KACxB,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,WAAmB;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,OAAO;gBAAE,SAAS;YACvB,MAAM,KAAK,GAAG,cAAc,CAC1B,OAAO,EACP,CAAC,KAAK,EAA8B,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EACtD,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,KAAK,uBAAuB,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,MAAM,CAAC,CACrF,CAAC;YACF,OAAO,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;QAClC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,mCAAmC,CAAC,WAAmB;IAC9D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QACpC,MAAM,OAAO,GAAG,GAAG;aAChB,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;aAC1B,MAAM,CAAC,OAAO,CAAC;aACf,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACZ,cAAc,CACZ,IAAI,EACJ,CAAC,KAAK,EAA8B,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EACtD,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,KAAK,uBAAuB,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,MAAM,CAAC,CACrF,CACF,CAAC;QAEJ,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,SAAS,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,WAAmB;IACrC,OAAO,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,4BAA4B,CACnC,iBAAyB,EACzB,WAA8B;IAE9B,MAAM,UAAU,GAAG,8BAA8B,CAAC,WAAW,CAAC,CAAC;IAC/D,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAE7B,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,UAAU,EAA+B,CAAC;IACjG,MAAM,UAAU,GAAG,oBAAoB,CAAC,OAAO,EAAE,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IACnF,IAAI,UAAU,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,qBAAqB,GAAG,OAAO,CAAC,SAAS,CAC7C,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,UAAU,IAAI,kBAAkB,CAAC,KAAK,CAAC,CAClE,CAAC;IACF,MAAM,QAAQ,GAAG,qBAAqB,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC;IACvF,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,oBAAoB,CAC3B,OAAkC,EAClC,UAAkB,EAClB,QAAiB;IAEjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;YAAE,SAAS;QAEzC,MAAM,cAAc,GAAG,2BAA2B,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1E,IAAI,cAAc,KAAK,UAAU;YAAE,SAAS;QAE5C,MAAM,gBAAgB,GAAG,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;QAClD,IACE,QAAQ,KAAK,SAAS;YACtB,OAAO,gBAAgB,KAAK,QAAQ;YACpC,gBAAgB,GAAG,QAAQ,EAC3B,CAAC;YACD,SAAS;QACX,CAAC;QAED,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,CAAC,CAAC;AACZ,CAAC;AAED,SAAS,kBAAkB,CAAC,KAA8B;IACxD,OAAO,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,KAAK,MAAM,CAAC;AACpE,CAAC;AAED,SAAS,cAAc,CAAC,KAA8B;IACpD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC;IACvC,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IAChD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,IAAI,EAA4C,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;SAChF,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;SAC9B,IAAI,CAAC,MAAM,CAAC,CAAC;AAClB,CAAC;AAED,SAAS,8BAA8B,CAAC,WAA8B;IACpE,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,IAAI,WAAW,CAAC,IAAI,IAAI,SAAS,CAAC;IACxE,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;IACtC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO,2BAA2B,CAAC,IAAI,SAAS,MAAM,IAAI,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,yBAAyB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,OAAO,CAAC,8DAA8D,EAAE,EAAE,CAAC,CAAC;AAC1F,CAAC;AAED,SAAS,2BAA2B,CAAC,IAAY;IAC/C,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CACnC,iIAAiI,EACjI,EAAE,CACH,CAAC;IACF,OAAO,yBAAyB,CAAC,gBAAgB,CAAC,CAAC,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED,SAAS,qBAAqB,CAAC,UAAkB;IAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC;IAC3D,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,OAAO,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,UAAkB;IACzD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC;IACnD,IAAI,QAAQ,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IACpF,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,WAAmB;IACzD,OAAO,UAAU,CAAC,WAAW,CAAC,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AACvF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,UAAkB;IAC1D,OAAO,wBAAwB,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,iBAAyB,EACzB,iBAAyB,EACzB,GAAW;IAEX,MAAM,UAAU,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IACjD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC;IAC3E,MAAM,UAAU,GAAG,MAAM,CAAC,cAAc,EAAE,CAAC;IAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,+BAA+B,iBAAiB,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,UAAU,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;IAC1C,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,sCAAsC,CACpD,iBAAyB,EACzB,GAAW,EACX,WAA8B,EAC9B,aAAsB;IAEtB,MAAM,UAAU,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IACjD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3C,MAAM,MAAM,GAAG;QACb,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,CAAC;QACV,EAAE,EAAE,UAAU,EAAE;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,GAAG;QACH,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5C,CAAC;IACF,MAAM,QAAQ,GAAG,8BAA8B,CAAC,WAAW,CAAC,CAAC;IAC7D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,sBAAsB,CAAC,iBAAiB,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzE,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IAED,MAAM,SAAS,GAAG;QAChB,IAAI,EAAE,SAAS;QACf,EAAE,EAAE,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5B,QAAQ,EAAE,IAAI;QACd,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,OAAO,EAAE;YACP,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;YAC3C,GAAG,CAAC,WAAW,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnF;KACF,CAAC;IACF,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrF,sBAAsB,CAAC,iBAAiB,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;IAC1D,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,oCAAoC,CAClD,iBAAyB,EACzB,iBAAyB,EACzB,GAAW,EACX,WAA8B;IAE9B,MAAM,eAAe,GAAG,4BAA4B,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;IACrF,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,IAAI,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,UAAU,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IACjD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3C,MAAM,MAAM,GAAG;QACb,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,CAAC;QACV,EAAE,EAAE,UAAU,EAAE;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,GAAG;QACH,aAAa,EAAE,iBAAiB;KACjC,CAAC;IACF,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,GAAG,eAAe,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9F,sBAAsB,CAAC,iBAAiB,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;IAC1D,OAAO,iBAAiB,CAAC;AAC3B,CAAC","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { existsSync, mkdirSync, renameSync, rmSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"../file-guards.js\";\nimport { atomicWritePrivateFile } from \"../fs-atomic.js\";\nimport { isPlatformHistorySession } from \"./metadata.js\";\n\nexport class ThreadRootNotFoundError extends Error {\n constructor(sessionFile: string) {\n super(`Thread root message not found in source session: ${sessionFile}`);\n this.name = \"ThreadRootNotFoundError\";\n }\n}\n\nexport interface ThreadRootMessage {\n text?: string;\n userName?: string;\n user?: string;\n loggedAt?: number;\n}\n\nexport interface ResolvedSessionScope {\n sessionDir: string;\n contextFile: string;\n threadRootMessage: ThreadRootMessage | null;\n}\n\nexport interface ResolveGenericSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\ninterface SessionMessageEntryLike {\n type: string;\n id: string;\n parentId: string | null;\n timestamp: string;\n message?: {\n role?: string;\n timestamp?: number;\n content?: Array<{ type?: string; text?: string }> | string;\n };\n}\n\n/**\n * Returns the shared session directory for a conversation.\n * Channel sessions use a current pointer within this directory.\n * Thread sessions are stored as fixed files within the same directory.\n */\nexport function getChannelSessionDir(channelDir: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Resolves the current active session file for a session directory.\n * Reads the \"current\" pointer file; creates a new session if none exists\n * or the pointed-to file is missing.\n */\nexport function resolveSessionFile(sessionDir: string): string {\n const existing = tryResolveCurrentSession(sessionDir);\n if (existing) return existing;\n return createNewSessionFile(sessionDir);\n}\n\n/**\n * Resolve the current active session file for a session directory.\n * Creates a fully initialized persistent session with the provided cwd when none exists.\n */\nexport function resolveManagedSessionFile(sessionDir: string, cwd: string): string {\n const existingPath = getCurrentSessionPath(sessionDir);\n if (existingPath && !isPlatformHistorySession(existingPath)) return existingPath;\n return createManagedSessionFile(sessionDir, cwd);\n}\n\n/**\n * Extracts the short UUID from a session file path.\n * e.g. \"2026-04-05T00-00_7b54cf90.jsonl\" → \"7b54cf90\"\n */\nexport function extractSessionUuid(sessionFile: string): string {\n const base = sessionFile.split(\"/\").pop() ?? sessionFile;\n return base.replace(\".jsonl\", \"\").split(\"_\").pop() ?? base;\n}\n\n/**\n * Extracts the thread/suffix part of a session key.\n * \"channelId:threadId\" → \"threadId\", \"channelId\" → \"channelId\"\n */\nexport function extractSessionSuffix(sessionKey: string): string {\n const parts = sessionKey.split(\":\");\n return parts.length > 1 ? parts[parts.length - 1] : sessionKey;\n}\n\n/**\n * Creates an empty timestamped file and updates the \"current\" pointer.\n * Used only by tests for placeholder-file scenarios.\n *\n * Order matters: write the session file first, then atomic-rename the pointer\n * last so a crash mid-create never leaves \"current\" pointing at a missing file.\n */\nexport function createNewSessionFile(sessionDir: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const uuid = randomUUID().slice(0, 8);\n const filename = `${timestamp}_${uuid}.jsonl`;\n const filePath = join(sessionDir, filename);\n atomicWritePrivateFile(filePath, \"\");\n atomicWritePrivateFile(join(sessionDir, \"current\"), filename);\n return filePath;\n}\n\n/**\n * Creates a new persistent session file with a proper SessionManager header and cwd.\n * Also updates the \"current\" pointer. Header is written before the pointer flips so a\n * partial create cannot leave \"current\" pointing at a missing file.\n */\nexport function createManagedSessionFile(sessionDir: string, cwd: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const sessionId = randomUUID();\n const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);\n writeSessionHeader(sessionFile, cwd, sessionId);\n setCurrentPointer(sessionDir, sessionFile);\n return sessionFile;\n}\n\n/**\n * Open a session file with an explicit cwd, even if the file does not exist yet.\n * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.\n */\nexport function openManagedSession(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n): SessionManager {\n if (shouldRecreatePreinitializedSession(sessionFile)) {\n rmSync(sessionFile, { force: true });\n }\n\n const SessionManagerCtor = SessionManager as unknown as {\n new (cwd: string, sessionDir: string, sessionFile: string, persist: boolean): SessionManager;\n };\n return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);\n}\n\nfunction setCurrentPointer(sessionDir: string, sessionFilePath: string): void {\n const filename = sessionFilePath.split(\"/\").pop()!;\n mkdirSync(sessionDir, { recursive: true });\n atomicWritePrivateFile(join(sessionDir, \"current\"), filename);\n}\n\n/**\n * Creates or overwrites a fixed-path session file with a valid session header.\n */\nexport function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string {\n writeSessionHeader(sessionFile, cwd);\n return sessionFile;\n}\n\nfunction writeSessionHeader(sessionFile: string, cwd: string, sessionId = randomUUID()): void {\n const sessionDir = getFileDir(sessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: new Date().toISOString(),\n cwd,\n };\n atomicWritePrivateFile(sessionFile, `${JSON.stringify(header)}\\n`);\n}\n\n/**\n * Returns the fixed session file path for a Slack thread.\n */\nexport function getThreadSessionFile(channelDir: string, sessionKey: string): string {\n return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);\n}\n\n/**\n * Resolve the default session scope for platforms without Slack-style branch forking.\n * Top-level/private sessions use the conversation's current pointer. Threaded or\n * per-message sessions use a fixed file derived from the session key suffix.\n */\nexport function resolveGenericSessionScope(\n options: ResolveGenericSessionScopeOptions,\n): ResolvedSessionScope {\n const { conversationDir, sessionKey } = options;\n const cwd = options.cwd ?? conversationDir;\n const sessionDir = getChannelSessionDir(conversationDir);\n\n if (!sessionKey.includes(\":\")) {\n return {\n sessionDir,\n contextFile: resolveManagedSessionFile(sessionDir, cwd),\n threadRootMessage: null,\n };\n }\n\n const threadFile = getThreadSessionFile(conversationDir, sessionKey);\n return {\n sessionDir,\n contextFile:\n tryResolveThreadSession(threadFile) ?? createManagedSessionFileAtPath(threadFile, cwd),\n threadRootMessage: null,\n };\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const raw = readTextFileIfExists(sessionFile);\n if (raw === undefined) return false;\n const lines = raw.split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = parseJsonValue(\n trimmed,\n (value): value is { type?: string } => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction shouldRecreatePreinitializedSession(sessionFile: string): boolean {\n try {\n const raw = readTextFileIfExists(sessionFile);\n if (raw === undefined) return false;\n const entries = raw\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) =>\n parseJsonValue(\n line,\n (value): value is { type?: string } => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n ),\n );\n\n return entries.length === 1 && entries[0]?.type === \"session\";\n } catch {\n return false;\n }\n}\n\nfunction getFileDir(sessionFile: string): string {\n return sessionFile.substring(0, sessionFile.lastIndexOf(\"/\"));\n}\n\nfunction resolveThreadSnapshotEntries(\n sourceSessionFile: string,\n rootMessage: ThreadRootMessage,\n): SessionMessageEntryLike[] | null {\n const targetText = buildComparableRootMessageText(rootMessage);\n if (!targetText) return null;\n\n const entries = SessionManager.open(sourceSessionFile).getEntries() as SessionMessageEntryLike[];\n const matchIndex = findRootMessageIndex(entries, targetText, rootMessage.loggedAt);\n if (matchIndex === -1) return null;\n\n const nextTopLevelUserIndex = entries.findIndex(\n (entry, index) => index > matchIndex && isUserMessageEntry(entry),\n );\n const endIndex = nextTopLevelUserIndex === -1 ? entries.length : nextTopLevelUserIndex;\n return entries.slice(0, endIndex);\n}\n\nfunction findRootMessageIndex(\n entries: SessionMessageEntryLike[],\n targetText: string,\n loggedAt?: number,\n): number {\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (!isUserMessageEntry(entry)) continue;\n\n const comparableText = normalizeComparableUserText(getMessageText(entry));\n if (comparableText !== targetText) continue;\n\n const messageTimestamp = entry.message?.timestamp;\n if (\n loggedAt !== undefined &&\n typeof messageTimestamp === \"number\" &&\n messageTimestamp < loggedAt\n ) {\n continue;\n }\n\n return i;\n }\n\n return -1;\n}\n\nfunction isUserMessageEntry(entry: SessionMessageEntryLike): boolean {\n return entry.type === \"message\" && entry.message?.role === \"user\";\n}\n\nfunction getMessageText(entry: SessionMessageEntryLike): string {\n const content = entry.message?.content;\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n return content\n .filter((part): part is { type?: string; text?: string } => part.type === \"text\")\n .map((part) => part.text ?? \"\")\n .join(\"\\n\\n\");\n}\n\nfunction buildComparableRootMessageText(rootMessage: ThreadRootMessage): string | null {\n const userLabel = rootMessage.userName || rootMessage.user || \"unknown\";\n const text = rootMessage.text?.trim();\n if (!text) return null;\n return normalizeComparableUserText(`[${userLabel}]: ${text}`);\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction getCurrentSessionPath(sessionDir: string): string | null {\n const pointerFile = join(sessionDir, \"current\");\n const filename = readTextFileIfExists(pointerFile)?.trim();\n if (!filename) return null;\n return join(sessionDir, filename);\n}\n\n/**\n * Try to resolve an existing current session file.\n * Returns null if no current pointer exists or the pointed file has no valid session header.\n */\nexport function tryResolveCurrentSession(sessionDir: string): string | null {\n const fullPath = getCurrentSessionPath(sessionDir);\n if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath)) return fullPath;\n return null;\n}\n\n/**\n * Try to resolve an existing thread session file.\n * Returns the file path if found, or null if no valid thread session exists yet.\n */\nexport function tryResolveThreadSession(sessionFile: string): string | null {\n return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;\n}\n\n/**\n * Resolve the channel's current session file path (for fork source).\n * Returns null if no channel session exists.\n */\nexport function resolveChannelSessionFile(channelDir: string): string | null {\n return tryResolveCurrentSession(getChannelSessionDir(channelDir));\n}\n\n/**\n * Fork a channel session into a fixed thread-session path.\n * The resulting file keeps forkFrom's distinct session/header metadata.\n */\nexport function forkThreadSessionFile(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);\n const forkedFile = forked.getSessionFile();\n if (!forkedFile) {\n throw new Error(`Failed to fork session from ${sourceSessionFile}`);\n }\n rmSync(targetSessionFile, { force: true });\n renameSync(forkedFile, targetSessionFile);\n return targetSessionFile;\n}\n\nexport function createThreadSessionFileFromRootMessage(\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n parentSession?: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n ...(parentSession ? { parentSession } : {}),\n };\n const rootText = buildComparableRootMessageText(rootMessage);\n if (!rootText) {\n atomicWritePrivateFile(targetSessionFile, `${JSON.stringify(header)}\\n`);\n return targetSessionFile;\n }\n\n const rootEntry = {\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date().toISOString(),\n message: {\n role: \"user\",\n content: [{ type: \"text\", text: rootText }],\n ...(rootMessage.loggedAt !== undefined ? { timestamp: rootMessage.loggedAt } : {}),\n },\n };\n const content = [header, rootEntry].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(targetSessionFile, `${content}\\n`);\n return targetSessionFile;\n}\n\nexport function forkThreadSessionFileFromRootMessage(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n): string {\n const snapshotEntries = resolveThreadSnapshotEntries(sourceSessionFile, rootMessage);\n if (!snapshotEntries) {\n throw new ThreadRootNotFoundError(sourceSessionFile);\n }\n\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n parentSession: sourceSessionFile,\n };\n const content = [header, ...snapshotEntries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(targetSessionFile, `${content}\\n`);\n return targetSessionFile;\n}\n"]}
@@ -0,0 +1,58 @@
1
+ export interface Attachment {
2
+ original: string;
3
+ localPath: string;
4
+ }
5
+ export interface LoggedMessage {
6
+ date: string;
7
+ ts: string;
8
+ user: string;
9
+ userName?: string;
10
+ displayName?: string;
11
+ text: string;
12
+ attachments: Attachment[];
13
+ isBot: boolean;
14
+ threadTs?: string;
15
+ }
16
+ export interface ChannelStoreConfig {
17
+ workingDir: string;
18
+ botToken: string;
19
+ }
20
+ export declare class ChannelStore {
21
+ private workingDir;
22
+ private botToken;
23
+ private recentlyLogged;
24
+ constructor(config: ChannelStoreConfig);
25
+ /**
26
+ * Get or create the directory for a channel/DM
27
+ */
28
+ getChannelDir(channelId: string): string;
29
+ /**
30
+ * Generate a unique local filename for an attachment
31
+ */
32
+ generateLocalFilename(originalName: string, timestamp: string): string;
33
+ /**
34
+ * Process attachments from a Slack message event.
35
+ * Downloads files before returning so callers only receive readable paths.
36
+ */
37
+ processAttachments(channelId: string, files: Array<{
38
+ name?: string;
39
+ url_private_download?: string;
40
+ url_private?: string;
41
+ }>, timestamp: string): Promise<Attachment[]>;
42
+ /**
43
+ * Log a message to the channel's log.jsonl
44
+ * Returns false if message was already logged (duplicate)
45
+ */
46
+ logMessage(channelId: string, message: LoggedMessage): Promise<boolean>;
47
+ /**
48
+ * Log a bot response
49
+ */
50
+ logBotResponse(channelId: string, text: string, ts: string): Promise<void>;
51
+ /**
52
+ * Get the timestamp of the last logged message for a channel
53
+ * Returns null if no log exists
54
+ */
55
+ getLastTimestamp(channelId: string): string | null;
56
+ private downloadAttachment;
57
+ }
58
+ //# sourceMappingURL=store.d.ts.map