@geminixiang/mama 0.2.0-beta.5 → 0.2.0-beta.7

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 (94) hide show
  1. package/README.md +81 -18
  2. package/dist/adapter.d.ts +3 -0
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +1 -1
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +84 -17
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/slack/bot.d.ts +12 -0
  10. package/dist/adapters/slack/bot.d.ts.map +1 -1
  11. package/dist/adapters/slack/bot.js +219 -15
  12. package/dist/adapters/slack/bot.js.map +1 -1
  13. package/dist/adapters/slack/context.d.ts.map +1 -1
  14. package/dist/adapters/slack/context.js +5 -0
  15. package/dist/adapters/slack/context.js.map +1 -1
  16. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  17. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  18. package/dist/adapters/slack/tools/attach.js.map +1 -1
  19. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  20. package/dist/adapters/telegram/bot.js +32 -35
  21. package/dist/adapters/telegram/bot.js.map +1 -1
  22. package/dist/agent.d.ts.map +1 -1
  23. package/dist/agent.js +42 -26
  24. package/dist/agent.js.map +1 -1
  25. package/dist/commands/index.d.ts.map +1 -1
  26. package/dist/commands/index.js +10 -1
  27. package/dist/commands/index.js.map +1 -1
  28. package/dist/commands/model.d.ts +14 -0
  29. package/dist/commands/model.d.ts.map +1 -0
  30. package/dist/commands/model.js +112 -0
  31. package/dist/commands/model.js.map +1 -0
  32. package/dist/commands/new.d.ts +9 -0
  33. package/dist/commands/new.d.ts.map +1 -0
  34. package/dist/commands/new.js +28 -0
  35. package/dist/commands/new.js.map +1 -0
  36. package/dist/commands/sandbox.d.ts +10 -0
  37. package/dist/commands/sandbox.d.ts.map +1 -0
  38. package/dist/commands/sandbox.js +65 -0
  39. package/dist/commands/sandbox.js.map +1 -0
  40. package/dist/commands/session-view.d.ts.map +1 -1
  41. package/dist/commands/session-view.js +29 -9
  42. package/dist/commands/session-view.js.map +1 -1
  43. package/dist/commands/types.d.ts +2 -0
  44. package/dist/commands/types.d.ts.map +1 -1
  45. package/dist/commands/types.js.map +1 -1
  46. package/dist/commands/utils.d.ts +3 -0
  47. package/dist/commands/utils.d.ts.map +1 -1
  48. package/dist/commands/utils.js +5 -0
  49. package/dist/commands/utils.js.map +1 -1
  50. package/dist/config.d.ts +13 -4
  51. package/dist/config.d.ts.map +1 -1
  52. package/dist/config.js +177 -31
  53. package/dist/config.js.map +1 -1
  54. package/dist/context.d.ts +1 -1
  55. package/dist/context.d.ts.map +1 -1
  56. package/dist/context.js +50 -35
  57. package/dist/context.js.map +1 -1
  58. package/dist/main.d.ts.map +1 -1
  59. package/dist/main.js +53 -4
  60. package/dist/main.js.map +1 -1
  61. package/dist/provisioner.d.ts +12 -0
  62. package/dist/provisioner.d.ts.map +1 -1
  63. package/dist/provisioner.js +41 -10
  64. package/dist/provisioner.js.map +1 -1
  65. package/dist/runtime/session-runtime.d.ts +1 -0
  66. package/dist/runtime/session-runtime.d.ts.map +1 -1
  67. package/dist/runtime/session-runtime.js +18 -0
  68. package/dist/runtime/session-runtime.js.map +1 -1
  69. package/dist/session-store.d.ts +1 -1
  70. package/dist/session-store.d.ts.map +1 -1
  71. package/dist/session-store.js +1 -1
  72. package/dist/session-store.js.map +1 -1
  73. package/dist/session-view/service.d.ts.map +1 -1
  74. package/dist/session-view/service.js +1 -1
  75. package/dist/session-view/service.js.map +1 -1
  76. package/dist/tools/bash.d.ts +1 -1
  77. package/dist/tools/bash.d.ts.map +1 -1
  78. package/dist/tools/bash.js.map +1 -1
  79. package/dist/tools/edit.d.ts +1 -1
  80. package/dist/tools/edit.d.ts.map +1 -1
  81. package/dist/tools/edit.js.map +1 -1
  82. package/dist/tools/event.d.ts +1 -1
  83. package/dist/tools/event.d.ts.map +1 -1
  84. package/dist/tools/event.js.map +1 -1
  85. package/dist/tools/index.d.ts +1 -1
  86. package/dist/tools/index.d.ts.map +1 -1
  87. package/dist/tools/index.js.map +1 -1
  88. package/dist/tools/read.d.ts +1 -1
  89. package/dist/tools/read.d.ts.map +1 -1
  90. package/dist/tools/read.js.map +1 -1
  91. package/dist/tools/write.d.ts +1 -1
  92. package/dist/tools/write.d.ts.map +1 -1
  93. package/dist/tools/write.js.map +1 -1
  94. package/package.json +4 -4
@@ -24,11 +24,13 @@ export class DockerContainerManager {
24
24
  this.image = image;
25
25
  this.state = new Map();
26
26
  this.inflight = new Map();
27
+ this.boostedKeys = new Set();
27
28
  if (typeof options === "function") {
28
29
  this.execFileImpl = options;
29
30
  }
30
31
  else {
31
32
  this.limits = options.limits;
33
+ this.boostLimits = options.boostLimits;
32
34
  this.execFileImpl = options.execFileImpl ?? execFileAsync;
33
35
  }
34
36
  }
@@ -84,14 +86,36 @@ export class DockerContainerManager {
84
86
  throw err;
85
87
  }
86
88
  this.setState(containerKey, "running", containerName);
87
- await this.applyResourceLimits(containerName);
89
+ await this.applyResourceLimits(containerKey, containerName);
88
90
  return containerName;
89
91
  }
92
+ async boost(containerKey) {
93
+ if (!this.boostLimits?.cpus && !this.boostLimits?.memory) {
94
+ return this.getLimitStatus(containerKey);
95
+ }
96
+ this.boostedKeys.add(containerKey);
97
+ const state = this.state.get(containerKey);
98
+ if (state?.status === "running") {
99
+ await this.applyResourceLimits(containerKey, state.containerName);
100
+ }
101
+ return this.getLimitStatus(containerKey);
102
+ }
103
+ getLimitStatus(containerKey) {
104
+ const boosted = this.boostedKeys.has(containerKey);
105
+ return { limits: this.effectiveLimits(containerKey), boosted };
106
+ }
107
+ getDefaultLimits() {
108
+ return this.limits;
109
+ }
110
+ getBoostLimits() {
111
+ return this.boostLimits;
112
+ }
90
113
  async stop(containerKey) {
91
114
  const containerName = this.getContainerName(containerKey);
92
115
  try {
93
116
  await this.execFileImpl("docker", ["stop", containerName]);
94
117
  this.setState(containerKey, "stopped", containerName);
118
+ this.boostedKeys.delete(containerKey);
95
119
  log.logInfo(`Container ${containerName} stopped (idle)`);
96
120
  }
97
121
  catch (err) {
@@ -110,6 +134,7 @@ export class DockerContainerManager {
110
134
  log.logWarning(`Failed to remove network ${networkName}`, err instanceof Error ? err.message : String(err));
111
135
  }
112
136
  this.state.delete(containerKey);
137
+ this.boostedKeys.delete(containerKey);
113
138
  }
114
139
  async stopIdle(maxIdleMs) {
115
140
  const now = Date.now();
@@ -191,25 +216,31 @@ export class DockerContainerManager {
191
216
  "--network",
192
217
  networkName,
193
218
  ...labels,
194
- ...this.resourceLimitArgs(),
219
+ ...this.resourceLimitArgs(this.effectiveLimits(containerKey)),
195
220
  ...this.mountArgs(mounts),
196
221
  this.image,
197
222
  "sleep",
198
223
  "infinity",
199
224
  ]);
200
225
  }
201
- resourceLimitArgs() {
226
+ effectiveLimits(containerKey) {
227
+ if (!this.boostedKeys.has(containerKey))
228
+ return this.limits;
229
+ return { ...this.limits, ...this.boostLimits };
230
+ }
231
+ resourceLimitArgs(limits) {
202
232
  const args = [];
203
- if (this.limits?.cpus)
204
- args.push("--cpus", this.limits.cpus);
205
- if (this.limits?.memory)
206
- args.push("--memory", this.limits.memory);
233
+ if (limits?.cpus)
234
+ args.push("--cpus", limits.cpus);
235
+ if (limits?.memory)
236
+ args.push("--memory", limits.memory);
207
237
  return args;
208
238
  }
209
- async applyResourceLimits(containerName) {
210
- if (!this.limits?.cpus && !this.limits?.memory)
239
+ async applyResourceLimits(containerKey, containerName) {
240
+ const limitArgs = this.resourceLimitArgs(this.effectiveLimits(containerKey));
241
+ if (limitArgs.length === 0)
211
242
  return;
212
- const args = ["update", ...this.resourceLimitArgs(), containerName];
243
+ const args = ["update", ...limitArgs, containerName];
213
244
  try {
214
245
  await this.execFileImpl("docker", args);
215
246
  }
@@ -1 +1 @@
1
- {"version":3,"file":"provisioner.js","sourceRoot":"","sources":["../src/provisioner.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"child_process\";\nimport { promisify } from \"util\";\nimport * as log from \"./log.js\";\n\nconst execFileAsync = promisify(execFile);\ntype ExecFileAsync = typeof execFileAsync;\n\ntype ContainerStatus = \"running\" | \"stopped\" | \"missing\";\n\nfunction isDockerNotFoundError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const stderr = (err as { stderr?: unknown }).stderr;\n const message = (err as { message?: unknown }).message;\n const haystack = `${typeof stderr === \"string\" ? stderr : \"\"}\\n${\n typeof message === \"string\" ? message : \"\"\n }`.toLowerCase();\n return (\n haystack.includes(\"no such network\") ||\n haystack.includes(\"no such container\") ||\n haystack.includes(\"no such object\") ||\n haystack.includes(\"network not found\") ||\n /network [^\\n]+ not found/.test(haystack) ||\n /error: no such [^\\n]+/.test(haystack)\n );\n}\n\ninterface ContainerState {\n status: ContainerStatus;\n lastUsed: number;\n containerName: string;\n}\n\nexport interface ContainerMount {\n source: string;\n target: string;\n}\n\nexport interface ResourceLimits {\n cpus?: string;\n memory?: string;\n}\n\nexport interface ProvisionOptions {\n containerName?: string;\n mounts?: ContainerMount[];\n conversationId?: string;\n}\n\nexport interface DockerContainerManagerOptions {\n limits?: ResourceLimits;\n execFileImpl?: ExecFileAsync;\n}\n\nexport class DockerContainerManager {\n private state = new Map<string, ContainerState>();\n private inflight = new Map<string, Promise<string>>();\n private static readonly MANAGED_LABEL = \"mama.managed=true\";\n private static readonly IMAGE_MODE_LABEL = \"mama.sandbox=image\";\n private static readonly VAULT_ID_LABEL_KEY = \"mama.vault-id\";\n private static readonly CONVERSATION_ID_LABEL_KEY = \"mama.conversation-id\";\n\n private readonly limits?: ResourceLimits;\n private readonly execFileImpl: ExecFileAsync;\n\n constructor(\n private readonly image: string,\n options: DockerContainerManagerOptions | ExecFileAsync = {},\n ) {\n if (typeof options === \"function\") {\n this.execFileImpl = options;\n } else {\n this.limits = options.limits;\n this.execFileImpl = options.execFileImpl ?? execFileAsync;\n }\n }\n\n static sanitizeSegment(value: string): string {\n const sanitized = value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return sanitized || \"unknown\";\n }\n\n static containerName(containerKey: string): string {\n return `mama-sandbox-${containerKey}`;\n }\n\n static networkName(containerKey: string): string {\n return `mama-sandbox-net-${containerKey}`;\n }\n\n async provision(containerKey: string, options: ProvisionOptions = {}): Promise<string> {\n const existing = this.inflight.get(containerKey);\n if (existing) return existing;\n\n const pending = this.provisionInner(containerKey, options).finally(() => {\n this.inflight.delete(containerKey);\n });\n this.inflight.set(containerKey, pending);\n return pending;\n }\n\n private async provisionInner(containerKey: string, options: ProvisionOptions): Promise<string> {\n const containerName =\n options.containerName ?? DockerContainerManager.containerName(containerKey);\n const mounts = options.mounts ?? [];\n const status = await this.inspectStatus(containerName);\n\n try {\n if (\n status !== \"missing\" &&\n (await this.hasRuntimeDrift(containerKey, containerName, mounts))\n ) {\n log.logInfo(`Container ${containerName} configuration changed; recreating container`);\n await this.execFileImpl(\"docker\", [\"rm\", \"-f\", containerName]);\n await this.runContainer(containerKey, containerName, mounts, options);\n log.logInfo(`Container ${containerName} recreated`);\n } else if (status === \"running\") {\n log.logInfo(`Container ${containerName} already running`);\n } else if (status === \"stopped\") {\n await this.execFileImpl(\"docker\", [\"start\", containerName]);\n log.logInfo(`Container ${containerName} started`);\n } else {\n await this.runContainer(containerKey, containerName, mounts, options);\n log.logInfo(`Container ${containerName} created`);\n }\n } catch (err) {\n this.state.delete(containerKey);\n throw err;\n }\n\n this.setState(containerKey, \"running\", containerName);\n await this.applyResourceLimits(containerName);\n return containerName;\n }\n\n async stop(containerKey: string): Promise<void> {\n const containerName = this.getContainerName(containerKey);\n try {\n await this.execFileImpl(\"docker\", [\"stop\", containerName]);\n this.setState(containerKey, \"stopped\", containerName);\n log.logInfo(`Container ${containerName} stopped (idle)`);\n } catch (err) {\n log.logWarning(\n `Failed to stop container ${containerName}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n async remove(containerKey: string): Promise<void> {\n const containerName = this.getContainerName(containerKey);\n const networkName = DockerContainerManager.networkName(containerKey);\n\n await this.forceRemoveContainer(\n containerName,\n `Container ${containerName} removed`,\n `Failed to remove container ${containerName}`,\n );\n\n try {\n await this.execFileImpl(\"docker\", [\"network\", \"rm\", networkName]);\n log.logInfo(`Network ${networkName} removed`);\n } catch (err) {\n log.logWarning(\n `Failed to remove network ${networkName}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n\n this.state.delete(containerKey);\n }\n\n async stopIdle(maxIdleMs: number): Promise<void> {\n const now = Date.now();\n const toStop: string[] = [];\n for (const [containerKey, containerState] of this.state) {\n if (containerState.status === \"running\" && now - containerState.lastUsed > maxIdleMs) {\n toStop.push(containerKey);\n }\n }\n await Promise.all(toStop.map((containerKey) => this.stop(containerKey)));\n }\n\n async reconcile(): Promise<void> {\n const discovered = new Set<string>();\n const labeledNames = await this.listContainerNamesByLabel();\n for (const name of labeledNames) discovered.add(name);\n const legacyNames = await this.listContainerNamesByPrefix();\n for (const name of legacyNames) discovered.add(name);\n\n this.state.clear();\n\n const inspected = await Promise.all(\n Array.from(discovered).map(async (containerName) => ({\n containerName,\n details: await this.inspectContainerDetails(containerName),\n })),\n );\n\n const legacyRemovals: Promise<void>[] = [];\n for (const { containerName, details } of inspected) {\n if (!details) continue;\n\n if (!details.conversationId) {\n legacyRemovals.push(this.removeLegacyContainer(containerName));\n continue;\n }\n\n const containerKey = this.containerKeyFromContainerName(containerName);\n if (!containerKey) {\n log.logWarning(`Skipping unmanaged-style container without container key`, containerName);\n continue;\n }\n\n const status: ContainerStatus = details.running ? \"running\" : \"stopped\";\n const lastUsed = details.startedAtMs ?? Date.now();\n this.state.set(containerKey, { status, lastUsed, containerName });\n }\n await Promise.all(legacyRemovals);\n\n const running = Array.from(this.state.values()).filter((s) => s.status === \"running\").length;\n const stopped = this.state.size - running;\n log.logInfo(\n `Reconciled ${this.state.size} managed containers (running=${running}, stopped=${stopped})`,\n );\n }\n\n private setState(containerKey: string, status: ContainerStatus, containerName: string): void {\n this.state.set(containerKey, { status, lastUsed: Date.now(), containerName });\n }\n\n private getContainerName(containerKey: string): string {\n return (\n this.state.get(containerKey)?.containerName ??\n DockerContainerManager.containerName(containerKey)\n );\n }\n\n private mountArgs(mounts: ContainerMount[]): string[] {\n return mounts.flatMap((mount) => [\"-v\", this.toBindSpec(mount)]);\n }\n\n private toBindSpec(mount: ContainerMount): string {\n return `${mount.source}:${mount.target}`;\n }\n\n private async runContainer(\n containerKey: string,\n containerName: string,\n mounts: ContainerMount[],\n options: ProvisionOptions,\n ): Promise<void> {\n const networkName = await this.ensureNetwork(containerKey);\n log.logInfo(`Creating container ${containerName} from image ${this.image}`);\n const labels = [\n \"--label\",\n DockerContainerManager.MANAGED_LABEL,\n \"--label\",\n DockerContainerManager.IMAGE_MODE_LABEL,\n \"--label\",\n `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,\n ];\n if (options.conversationId) {\n labels.push(\n \"--label\",\n `${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}=${options.conversationId}`,\n );\n }\n await this.execFileImpl(\"docker\", [\n \"run\",\n \"-d\",\n \"--name\",\n containerName,\n \"--network\",\n networkName,\n ...labels,\n ...this.resourceLimitArgs(),\n ...this.mountArgs(mounts),\n this.image,\n \"sleep\",\n \"infinity\",\n ]);\n }\n\n private resourceLimitArgs(): string[] {\n const args: string[] = [];\n if (this.limits?.cpus) args.push(\"--cpus\", this.limits.cpus);\n if (this.limits?.memory) args.push(\"--memory\", this.limits.memory);\n return args;\n }\n\n private async applyResourceLimits(containerName: string): Promise<void> {\n if (!this.limits?.cpus && !this.limits?.memory) return;\n const args = [\"update\", ...this.resourceLimitArgs(), containerName];\n try {\n await this.execFileImpl(\"docker\", args);\n } catch (err) {\n log.logWarning(\n `Failed to apply resource limits to container ${containerName}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n private async hasRuntimeDrift(\n containerKey: string,\n containerName: string,\n mounts: ContainerMount[],\n ): Promise<boolean> {\n if (await this.hasBindMountDrift(containerName, mounts)) {\n return true;\n }\n return this.hasNetworkModeDrift(containerKey, containerName);\n }\n\n private async hasBindMountDrift(\n containerName: string,\n mounts: ContainerMount[],\n ): Promise<boolean> {\n const expected = this.expectedBinds(mounts);\n const actual = await this.inspectBindMounts(containerName);\n return !this.sameBinds(expected, actual);\n }\n\n private expectedBinds(mounts: ContainerMount[]): string[] {\n return mounts\n .map((mount) => this.toBindSpec(mount))\n .slice()\n .sort();\n }\n\n private sameBinds(expected: string[], actual: string[]): boolean {\n if (expected.length !== actual.length) {\n return false;\n }\n\n return expected.every((bind, index) => bind === actual[index]);\n }\n\n private async inspectBindMounts(containerName: string): Promise<string[]> {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{json .HostConfig.Binds}}\",\n containerName,\n ]);\n const payload = stdout.trim();\n const parsed = JSON.parse(payload.length > 0 ? payload : \"null\") as unknown;\n\n if (parsed === null) {\n return [];\n }\n\n if (!Array.isArray(parsed) || parsed.some((bind) => typeof bind !== \"string\")) {\n throw new Error(`Unexpected docker bind mount payload for container \"${containerName}\"`);\n }\n\n return [...parsed].sort();\n }\n\n private async hasNetworkModeDrift(containerKey: string, containerName: string): Promise<boolean> {\n const expected = DockerContainerManager.networkName(containerKey);\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.HostConfig.NetworkMode}}\",\n containerName,\n ]);\n return stdout.trim() !== expected;\n }\n\n private async ensureNetwork(containerKey: string): Promise<string> {\n const networkName = DockerContainerManager.networkName(containerKey);\n try {\n await this.execFileImpl(\"docker\", [\"network\", \"inspect\", networkName]);\n return networkName;\n } catch (err) {\n if (!isDockerNotFoundError(err)) throw err;\n }\n await this.execFileImpl(\"docker\", [\n \"network\",\n \"create\",\n \"--driver\",\n \"bridge\",\n \"--label\",\n DockerContainerManager.MANAGED_LABEL,\n \"--label\",\n DockerContainerManager.IMAGE_MODE_LABEL,\n \"--label\",\n `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,\n networkName,\n ]);\n return networkName;\n }\n\n private async inspectStatus(containerName: string): Promise<ContainerStatus> {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.State.Running}}\",\n containerName,\n ]);\n return stdout.trim() === \"true\" ? \"running\" : \"stopped\";\n } catch (err) {\n if (isDockerNotFoundError(err)) return \"missing\";\n throw err;\n }\n }\n\n private async listContainerNamesByLabel(): Promise<string[]> {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"ps\",\n \"-a\",\n \"--filter\",\n `label=${DockerContainerManager.MANAGED_LABEL}`,\n \"--filter\",\n `label=${DockerContainerManager.IMAGE_MODE_LABEL}`,\n \"--format\",\n \"{{.Names}}\",\n ]);\n return this.parseNameLines(stdout);\n } catch (err) {\n log.logWarning(\n \"Failed to list labeled managed containers\",\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n }\n\n private async listContainerNamesByPrefix(): Promise<string[]> {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"ps\",\n \"-a\",\n \"--filter\",\n `name=${DockerContainerManager.containerName(\"\")}`,\n \"--format\",\n \"{{.Names}}\",\n ]);\n return this.parseNameLines(stdout);\n } catch (err) {\n log.logWarning(\n \"Failed to list legacy managed containers\",\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n }\n\n private parseNameLines(stdout: string): string[] {\n return stdout\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n }\n\n private async inspectContainerDetails(\n containerName: string,\n ): Promise<\n | { running: boolean; startedAtMs?: number; vaultId?: string; conversationId?: string }\n | undefined\n > {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n `{{.State.Running}}\\t{{.State.StartedAt}}\\t{{index .Config.Labels \"${DockerContainerManager.VAULT_ID_LABEL_KEY}\"}}\\t{{index .Config.Labels \"${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}\"}}`,\n containerName,\n ]);\n const [runningRaw, startedAtRaw, vaultIdRaw, conversationIdRaw] = stdout.trim().split(\"\\t\");\n const running = runningRaw === \"true\";\n const startedAtMs = this.parseDockerTimestamp(startedAtRaw);\n const vaultId = this.normalizeDockerValue(vaultIdRaw);\n const conversationId = this.normalizeDockerValue(conversationIdRaw);\n return { running, startedAtMs, vaultId, conversationId };\n } catch (err) {\n log.logWarning(\n `Failed to inspect container ${containerName} during reconcile`,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n }\n\n private normalizeDockerValue(value?: string): string | undefined {\n if (!value || value === \"<no value>\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n private parseDockerTimestamp(value?: string): number | undefined {\n const normalized = this.normalizeDockerValue(value);\n if (!normalized || normalized.startsWith(\"0001-\")) return undefined;\n const parsed = Date.parse(normalized);\n return Number.isNaN(parsed) ? undefined : parsed;\n }\n\n private containerKeyFromContainerName(containerName: string): string | undefined {\n const prefix = DockerContainerManager.containerName(\"\");\n if (!containerName.startsWith(prefix)) return undefined;\n const containerKey = containerName.slice(prefix.length);\n return containerKey.length > 0 ? containerKey : undefined;\n }\n\n private async forceRemoveContainer(\n containerName: string,\n successLog: string,\n failureLog: string,\n ): Promise<void> {\n try {\n await this.execFileImpl(\"docker\", [\"rm\", \"-f\", containerName]);\n log.logInfo(successLog);\n } catch (err) {\n log.logWarning(failureLog, err instanceof Error ? err.message : String(err));\n }\n }\n\n private async removeLegacyContainer(containerName: string): Promise<void> {\n await this.forceRemoveContainer(\n containerName,\n `Removed legacy mama container ${containerName} (pre-channel-isolation scheme)`,\n `Failed to remove legacy mama container ${containerName}`,\n );\n }\n}\n\n/** @deprecated Use DockerContainerManager */\nexport const DockerProvisioner = DockerContainerManager;\n"]}
1
+ {"version":3,"file":"provisioner.js","sourceRoot":"","sources":["../src/provisioner.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"child_process\";\nimport { promisify } from \"util\";\nimport * as log from \"./log.js\";\n\nconst execFileAsync = promisify(execFile);\ntype ExecFileAsync = typeof execFileAsync;\n\ntype ContainerStatus = \"running\" | \"stopped\" | \"missing\";\n\nfunction isDockerNotFoundError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const stderr = (err as { stderr?: unknown }).stderr;\n const message = (err as { message?: unknown }).message;\n const haystack = `${typeof stderr === \"string\" ? stderr : \"\"}\\n${\n typeof message === \"string\" ? message : \"\"\n }`.toLowerCase();\n return (\n haystack.includes(\"no such network\") ||\n haystack.includes(\"no such container\") ||\n haystack.includes(\"no such object\") ||\n haystack.includes(\"network not found\") ||\n /network [^\\n]+ not found/.test(haystack) ||\n /error: no such [^\\n]+/.test(haystack)\n );\n}\n\ninterface ContainerState {\n status: ContainerStatus;\n lastUsed: number;\n containerName: string;\n}\n\nexport interface ContainerMount {\n source: string;\n target: string;\n}\n\nexport interface ResourceLimits {\n cpus?: string;\n memory?: string;\n}\n\nexport interface SandboxLimitStatus {\n limits?: ResourceLimits;\n boosted: boolean;\n}\n\nexport interface ProvisionOptions {\n containerName?: string;\n mounts?: ContainerMount[];\n conversationId?: string;\n}\n\nexport interface DockerContainerManagerOptions {\n limits?: ResourceLimits;\n boostLimits?: ResourceLimits;\n execFileImpl?: ExecFileAsync;\n}\n\nexport class DockerContainerManager {\n private state = new Map<string, ContainerState>();\n private inflight = new Map<string, Promise<string>>();\n private static readonly MANAGED_LABEL = \"mama.managed=true\";\n private static readonly IMAGE_MODE_LABEL = \"mama.sandbox=image\";\n private static readonly VAULT_ID_LABEL_KEY = \"mama.vault-id\";\n private static readonly CONVERSATION_ID_LABEL_KEY = \"mama.conversation-id\";\n\n private readonly limits?: ResourceLimits;\n private readonly boostLimits?: ResourceLimits;\n private readonly boostedKeys = new Set<string>();\n private readonly execFileImpl: ExecFileAsync;\n\n constructor(\n private readonly image: string,\n options: DockerContainerManagerOptions | ExecFileAsync = {},\n ) {\n if (typeof options === \"function\") {\n this.execFileImpl = options;\n } else {\n this.limits = options.limits;\n this.boostLimits = options.boostLimits;\n this.execFileImpl = options.execFileImpl ?? execFileAsync;\n }\n }\n\n static sanitizeSegment(value: string): string {\n const sanitized = value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return sanitized || \"unknown\";\n }\n\n static containerName(containerKey: string): string {\n return `mama-sandbox-${containerKey}`;\n }\n\n static networkName(containerKey: string): string {\n return `mama-sandbox-net-${containerKey}`;\n }\n\n async provision(containerKey: string, options: ProvisionOptions = {}): Promise<string> {\n const existing = this.inflight.get(containerKey);\n if (existing) return existing;\n\n const pending = this.provisionInner(containerKey, options).finally(() => {\n this.inflight.delete(containerKey);\n });\n this.inflight.set(containerKey, pending);\n return pending;\n }\n\n private async provisionInner(containerKey: string, options: ProvisionOptions): Promise<string> {\n const containerName =\n options.containerName ?? DockerContainerManager.containerName(containerKey);\n const mounts = options.mounts ?? [];\n const status = await this.inspectStatus(containerName);\n\n try {\n if (\n status !== \"missing\" &&\n (await this.hasRuntimeDrift(containerKey, containerName, mounts))\n ) {\n log.logInfo(`Container ${containerName} configuration changed; recreating container`);\n await this.execFileImpl(\"docker\", [\"rm\", \"-f\", containerName]);\n await this.runContainer(containerKey, containerName, mounts, options);\n log.logInfo(`Container ${containerName} recreated`);\n } else if (status === \"running\") {\n log.logInfo(`Container ${containerName} already running`);\n } else if (status === \"stopped\") {\n await this.execFileImpl(\"docker\", [\"start\", containerName]);\n log.logInfo(`Container ${containerName} started`);\n } else {\n await this.runContainer(containerKey, containerName, mounts, options);\n log.logInfo(`Container ${containerName} created`);\n }\n } catch (err) {\n this.state.delete(containerKey);\n throw err;\n }\n\n this.setState(containerKey, \"running\", containerName);\n await this.applyResourceLimits(containerKey, containerName);\n return containerName;\n }\n\n async boost(containerKey: string): Promise<SandboxLimitStatus> {\n if (!this.boostLimits?.cpus && !this.boostLimits?.memory) {\n return this.getLimitStatus(containerKey);\n }\n\n this.boostedKeys.add(containerKey);\n const state = this.state.get(containerKey);\n if (state?.status === \"running\") {\n await this.applyResourceLimits(containerKey, state.containerName);\n }\n return this.getLimitStatus(containerKey);\n }\n\n getLimitStatus(containerKey: string): SandboxLimitStatus {\n const boosted = this.boostedKeys.has(containerKey);\n return { limits: this.effectiveLimits(containerKey), boosted };\n }\n\n getDefaultLimits(): ResourceLimits | undefined {\n return this.limits;\n }\n\n getBoostLimits(): ResourceLimits | undefined {\n return this.boostLimits;\n }\n\n async stop(containerKey: string): Promise<void> {\n const containerName = this.getContainerName(containerKey);\n try {\n await this.execFileImpl(\"docker\", [\"stop\", containerName]);\n this.setState(containerKey, \"stopped\", containerName);\n this.boostedKeys.delete(containerKey);\n log.logInfo(`Container ${containerName} stopped (idle)`);\n } catch (err) {\n log.logWarning(\n `Failed to stop container ${containerName}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n async remove(containerKey: string): Promise<void> {\n const containerName = this.getContainerName(containerKey);\n const networkName = DockerContainerManager.networkName(containerKey);\n\n await this.forceRemoveContainer(\n containerName,\n `Container ${containerName} removed`,\n `Failed to remove container ${containerName}`,\n );\n\n try {\n await this.execFileImpl(\"docker\", [\"network\", \"rm\", networkName]);\n log.logInfo(`Network ${networkName} removed`);\n } catch (err) {\n log.logWarning(\n `Failed to remove network ${networkName}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n\n this.state.delete(containerKey);\n this.boostedKeys.delete(containerKey);\n }\n\n async stopIdle(maxIdleMs: number): Promise<void> {\n const now = Date.now();\n const toStop: string[] = [];\n for (const [containerKey, containerState] of this.state) {\n if (containerState.status === \"running\" && now - containerState.lastUsed > maxIdleMs) {\n toStop.push(containerKey);\n }\n }\n await Promise.all(toStop.map((containerKey) => this.stop(containerKey)));\n }\n\n async reconcile(): Promise<void> {\n const discovered = new Set<string>();\n const labeledNames = await this.listContainerNamesByLabel();\n for (const name of labeledNames) discovered.add(name);\n const legacyNames = await this.listContainerNamesByPrefix();\n for (const name of legacyNames) discovered.add(name);\n\n this.state.clear();\n\n const inspected = await Promise.all(\n Array.from(discovered).map(async (containerName) => ({\n containerName,\n details: await this.inspectContainerDetails(containerName),\n })),\n );\n\n const legacyRemovals: Promise<void>[] = [];\n for (const { containerName, details } of inspected) {\n if (!details) continue;\n\n if (!details.conversationId) {\n legacyRemovals.push(this.removeLegacyContainer(containerName));\n continue;\n }\n\n const containerKey = this.containerKeyFromContainerName(containerName);\n if (!containerKey) {\n log.logWarning(`Skipping unmanaged-style container without container key`, containerName);\n continue;\n }\n\n const status: ContainerStatus = details.running ? \"running\" : \"stopped\";\n const lastUsed = details.startedAtMs ?? Date.now();\n this.state.set(containerKey, { status, lastUsed, containerName });\n }\n await Promise.all(legacyRemovals);\n\n const running = Array.from(this.state.values()).filter((s) => s.status === \"running\").length;\n const stopped = this.state.size - running;\n log.logInfo(\n `Reconciled ${this.state.size} managed containers (running=${running}, stopped=${stopped})`,\n );\n }\n\n private setState(containerKey: string, status: ContainerStatus, containerName: string): void {\n this.state.set(containerKey, { status, lastUsed: Date.now(), containerName });\n }\n\n private getContainerName(containerKey: string): string {\n return (\n this.state.get(containerKey)?.containerName ??\n DockerContainerManager.containerName(containerKey)\n );\n }\n\n private mountArgs(mounts: ContainerMount[]): string[] {\n return mounts.flatMap((mount) => [\"-v\", this.toBindSpec(mount)]);\n }\n\n private toBindSpec(mount: ContainerMount): string {\n return `${mount.source}:${mount.target}`;\n }\n\n private async runContainer(\n containerKey: string,\n containerName: string,\n mounts: ContainerMount[],\n options: ProvisionOptions,\n ): Promise<void> {\n const networkName = await this.ensureNetwork(containerKey);\n log.logInfo(`Creating container ${containerName} from image ${this.image}`);\n const labels = [\n \"--label\",\n DockerContainerManager.MANAGED_LABEL,\n \"--label\",\n DockerContainerManager.IMAGE_MODE_LABEL,\n \"--label\",\n `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,\n ];\n if (options.conversationId) {\n labels.push(\n \"--label\",\n `${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}=${options.conversationId}`,\n );\n }\n await this.execFileImpl(\"docker\", [\n \"run\",\n \"-d\",\n \"--name\",\n containerName,\n \"--network\",\n networkName,\n ...labels,\n ...this.resourceLimitArgs(this.effectiveLimits(containerKey)),\n ...this.mountArgs(mounts),\n this.image,\n \"sleep\",\n \"infinity\",\n ]);\n }\n\n private effectiveLimits(containerKey: string): ResourceLimits | undefined {\n if (!this.boostedKeys.has(containerKey)) return this.limits;\n return { ...this.limits, ...this.boostLimits };\n }\n\n private resourceLimitArgs(limits: ResourceLimits | undefined): string[] {\n const args: string[] = [];\n if (limits?.cpus) args.push(\"--cpus\", limits.cpus);\n if (limits?.memory) args.push(\"--memory\", limits.memory);\n return args;\n }\n\n private async applyResourceLimits(containerKey: string, containerName: string): Promise<void> {\n const limitArgs = this.resourceLimitArgs(this.effectiveLimits(containerKey));\n if (limitArgs.length === 0) return;\n const args = [\"update\", ...limitArgs, containerName];\n try {\n await this.execFileImpl(\"docker\", args);\n } catch (err) {\n log.logWarning(\n `Failed to apply resource limits to container ${containerName}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n private async hasRuntimeDrift(\n containerKey: string,\n containerName: string,\n mounts: ContainerMount[],\n ): Promise<boolean> {\n if (await this.hasBindMountDrift(containerName, mounts)) {\n return true;\n }\n return this.hasNetworkModeDrift(containerKey, containerName);\n }\n\n private async hasBindMountDrift(\n containerName: string,\n mounts: ContainerMount[],\n ): Promise<boolean> {\n const expected = this.expectedBinds(mounts);\n const actual = await this.inspectBindMounts(containerName);\n return !this.sameBinds(expected, actual);\n }\n\n private expectedBinds(mounts: ContainerMount[]): string[] {\n return mounts\n .map((mount) => this.toBindSpec(mount))\n .slice()\n .sort();\n }\n\n private sameBinds(expected: string[], actual: string[]): boolean {\n if (expected.length !== actual.length) {\n return false;\n }\n\n return expected.every((bind, index) => bind === actual[index]);\n }\n\n private async inspectBindMounts(containerName: string): Promise<string[]> {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{json .HostConfig.Binds}}\",\n containerName,\n ]);\n const payload = stdout.trim();\n const parsed = JSON.parse(payload.length > 0 ? payload : \"null\") as unknown;\n\n if (parsed === null) {\n return [];\n }\n\n if (!Array.isArray(parsed) || parsed.some((bind) => typeof bind !== \"string\")) {\n throw new Error(`Unexpected docker bind mount payload for container \"${containerName}\"`);\n }\n\n return [...parsed].sort();\n }\n\n private async hasNetworkModeDrift(containerKey: string, containerName: string): Promise<boolean> {\n const expected = DockerContainerManager.networkName(containerKey);\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.HostConfig.NetworkMode}}\",\n containerName,\n ]);\n return stdout.trim() !== expected;\n }\n\n private async ensureNetwork(containerKey: string): Promise<string> {\n const networkName = DockerContainerManager.networkName(containerKey);\n try {\n await this.execFileImpl(\"docker\", [\"network\", \"inspect\", networkName]);\n return networkName;\n } catch (err) {\n if (!isDockerNotFoundError(err)) throw err;\n }\n await this.execFileImpl(\"docker\", [\n \"network\",\n \"create\",\n \"--driver\",\n \"bridge\",\n \"--label\",\n DockerContainerManager.MANAGED_LABEL,\n \"--label\",\n DockerContainerManager.IMAGE_MODE_LABEL,\n \"--label\",\n `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,\n networkName,\n ]);\n return networkName;\n }\n\n private async inspectStatus(containerName: string): Promise<ContainerStatus> {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.State.Running}}\",\n containerName,\n ]);\n return stdout.trim() === \"true\" ? \"running\" : \"stopped\";\n } catch (err) {\n if (isDockerNotFoundError(err)) return \"missing\";\n throw err;\n }\n }\n\n private async listContainerNamesByLabel(): Promise<string[]> {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"ps\",\n \"-a\",\n \"--filter\",\n `label=${DockerContainerManager.MANAGED_LABEL}`,\n \"--filter\",\n `label=${DockerContainerManager.IMAGE_MODE_LABEL}`,\n \"--format\",\n \"{{.Names}}\",\n ]);\n return this.parseNameLines(stdout);\n } catch (err) {\n log.logWarning(\n \"Failed to list labeled managed containers\",\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n }\n\n private async listContainerNamesByPrefix(): Promise<string[]> {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"ps\",\n \"-a\",\n \"--filter\",\n `name=${DockerContainerManager.containerName(\"\")}`,\n \"--format\",\n \"{{.Names}}\",\n ]);\n return this.parseNameLines(stdout);\n } catch (err) {\n log.logWarning(\n \"Failed to list legacy managed containers\",\n err instanceof Error ? err.message : String(err),\n );\n return [];\n }\n }\n\n private parseNameLines(stdout: string): string[] {\n return stdout\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n }\n\n private async inspectContainerDetails(\n containerName: string,\n ): Promise<\n | { running: boolean; startedAtMs?: number; vaultId?: string; conversationId?: string }\n | undefined\n > {\n try {\n const { stdout } = await this.execFileImpl(\"docker\", [\n \"inspect\",\n \"-f\",\n `{{.State.Running}}\\t{{.State.StartedAt}}\\t{{index .Config.Labels \"${DockerContainerManager.VAULT_ID_LABEL_KEY}\"}}\\t{{index .Config.Labels \"${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}\"}}`,\n containerName,\n ]);\n const [runningRaw, startedAtRaw, vaultIdRaw, conversationIdRaw] = stdout.trim().split(\"\\t\");\n const running = runningRaw === \"true\";\n const startedAtMs = this.parseDockerTimestamp(startedAtRaw);\n const vaultId = this.normalizeDockerValue(vaultIdRaw);\n const conversationId = this.normalizeDockerValue(conversationIdRaw);\n return { running, startedAtMs, vaultId, conversationId };\n } catch (err) {\n log.logWarning(\n `Failed to inspect container ${containerName} during reconcile`,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n }\n\n private normalizeDockerValue(value?: string): string | undefined {\n if (!value || value === \"<no value>\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n private parseDockerTimestamp(value?: string): number | undefined {\n const normalized = this.normalizeDockerValue(value);\n if (!normalized || normalized.startsWith(\"0001-\")) return undefined;\n const parsed = Date.parse(normalized);\n return Number.isNaN(parsed) ? undefined : parsed;\n }\n\n private containerKeyFromContainerName(containerName: string): string | undefined {\n const prefix = DockerContainerManager.containerName(\"\");\n if (!containerName.startsWith(prefix)) return undefined;\n const containerKey = containerName.slice(prefix.length);\n return containerKey.length > 0 ? containerKey : undefined;\n }\n\n private async forceRemoveContainer(\n containerName: string,\n successLog: string,\n failureLog: string,\n ): Promise<void> {\n try {\n await this.execFileImpl(\"docker\", [\"rm\", \"-f\", containerName]);\n log.logInfo(successLog);\n } catch (err) {\n log.logWarning(failureLog, err instanceof Error ? err.message : String(err));\n }\n }\n\n private async removeLegacyContainer(containerName: string): Promise<void> {\n await this.forceRemoveContainer(\n containerName,\n `Removed legacy mama container ${containerName} (pre-channel-isolation scheme)`,\n `Failed to remove legacy mama container ${containerName}`,\n );\n }\n}\n\n/** @deprecated Use DockerContainerManager */\nexport const DockerProvisioner = DockerContainerManager;\n"]}
@@ -20,6 +20,7 @@ export interface SessionRuntimeOptions extends CommandServices {
20
20
  export interface SessionRuntime extends BotHandler {
21
21
  runSession(options: RunSessionOptions): Promise<void>;
22
22
  createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner>;
23
+ switchConversationModel(conversationId: string, provider: string, model: string): boolean;
23
24
  shutdown(timeoutMs?: number): Promise<void>;
24
25
  }
25
26
  export declare function createSessionRuntime(options: SessionRuntimeOptions): SessionRuntime;
@@ -1 +1 @@
1
- {"version":3,"file":"session-runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/session-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,WAAW,EACX,QAAQ,EACR,UAAU,EAGX,MAAM,eAAe,CAAC;AAMvB,OAAO,EAAE,KAAK,WAAW,EAAgB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAgC,MAAM,sBAAsB,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA0B5D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,2EAA2E;IAC3E,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAKD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAEnF","sourcesContent":["import type {\n Bot,\n BotAdapters,\n BotEvent,\n BotHandler,\n PlatformName,\n RunningSession,\n} from \"../adapter.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"../adapters/slack/branch-manager.js\";\nimport { type AgentRunner, createRunner } from \"../agent.js\";\nimport { CommandRegistry, createDefaultCommandRegistry } from \"../commands/index.js\";\nimport type { CommandServices } from \"../commands/index.js\";\nimport { isPrivateConversation } from \"../commands/utils.js\";\nimport * as log from \"../log.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"../session-store.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"../sentry.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"../ui-copy.js\";\nimport * as Sentry from \"@sentry/node\";\nimport { join } from \"path\";\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nexport interface RunSessionOptions {\n event: BotEvent;\n bot: Bot;\n adapters: BotAdapters;\n isEvent?: boolean;\n}\n\nexport interface CreateSessionSandboxOptions {\n conversationId: string;\n platformName: string;\n sessionKey: string;\n}\n\nexport interface SessionRuntimeOptions extends CommandServices {\n /** Override the default command registry (e.g., to add /help, /status). */\n commandRegistry?: CommandRegistry;\n}\n\nexport interface SessionRuntime extends BotHandler {\n runSession(options: RunSessionOptions): Promise<void>;\n createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner>;\n shutdown(timeoutMs?: number): Promise<void>;\n}\n\nconst MAX_SESSIONS = 500;\nconst IDLE_TIMEOUT_MS = 3_600_000;\n\nexport function createSessionRuntime(options: SessionRuntimeOptions): SessionRuntime {\n return new MamaSessionRuntime(options);\n}\n\nclass MamaSessionRuntime implements SessionRuntime {\n private readonly conversationStates = new Map<string, ConversationState>();\n private readonly inFlightRuns = new Set<Promise<void>>();\n private readonly commandRegistry: CommandRegistry;\n private isShuttingDown = false;\n\n constructor(private readonly options: SessionRuntimeOptions) {\n this.commandRegistry = options.commandRegistry ?? createDefaultCommandRegistry();\n }\n\n isRunning(sessionKey: string): boolean {\n const state = this.conversationStates.get(sessionKey);\n return !!state?.running;\n }\n\n getRunningSessions(): RunningSession[] {\n const sessions: RunningSession[] = [];\n for (const [sessionKey, state] of this.conversationStates) {\n if (state.running && state.startedAt) {\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n }\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n }\n\n forceStop(sessionKey: string): void {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n }\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n this.conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n }\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n isEvent?: boolean,\n ): Promise<void> {\n await this.runSession({ event, bot, adapters, isEvent });\n }\n\n async runSession({ event, bot, adapters, isEvent }: RunSessionOptions): Promise<void> {\n const conversationId = event.conversationId;\n if (this.isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledCommand = await this.commandRegistry.handle({\n bot,\n responseCtx: adapters.responseCtx,\n platform: adapters.platform.name as PlatformName,\n platformUserId: event.user,\n conversationId,\n vaultConversationId: event.vaultConversationId,\n sessionKey,\n commandText: event.text,\n privateConversation,\n services: this.options,\n });\n if (handledCommand) return;\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => this.conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await this.getOrCreateState({\n conversationId,\n platformName: adapters.platform.name,\n sessionKey,\n });\n\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n const runPromise = (async () => {\n try {\n const result = await this.runWithInstrumentation(\n adapters,\n { conversationId, sessionKey, isEvent, startedAt: state.startedAt! },\n async () => {\n await adapters.responseCtx.setTyping(true);\n await adapters.responseCtx.setWorking(true);\n const r = await state.runner.run(\n adapters.message,\n adapters.responseCtx,\n adapters.platform,\n );\n await adapters.responseCtx.setWorking(false);\n return r;\n },\n );\n\n if (result?.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size - 1);\n this.evictIdleSessions();\n }\n })();\n\n this.inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n this.inFlightRuns.delete(runPromise);\n }\n }\n\n private async runWithInstrumentation(\n adapters: BotAdapters,\n meta: { conversationId: string; sessionKey: string; isEvent?: boolean; startedAt: number },\n body: () => Promise<{ stopReason: string; errorMessage?: string }>,\n ): Promise<{ stopReason: string; errorMessage?: string } | undefined> {\n const { conversationId, sessionKey, isEvent, startedAt } = meta;\n const { message, platform } = adapters;\n\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size + 1);\n\n return Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () =>\n Sentry.withScope(async (scope) => {\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n const result = await body();\n const durationMs = Date.now() - startedAt;\n const completionAttrs = {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n };\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: completionAttrs,\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, { attributes: completionAttrs });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n return result;\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: platform.name,\n messageId: message.id,\n threadTs: message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n }),\n );\n }\n\n async createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner> {\n const state = await this.getOrCreateState(options);\n return state.runner;\n }\n\n private async getOrCreateState({\n conversationId,\n platformName,\n sessionKey,\n }: CreateSessionSandboxOptions): Promise<ConversationState> {\n const existing = this.conversationStates.get(sessionKey);\n if (existing) {\n existing.lastAccessedAt = Date.now();\n return existing;\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const sessionScope = await this.resolveSessionScope(platformName, conversationDir, sessionKey);\n const state: ConversationState = {\n running: false,\n runner: await createRunner(\n this.options.sandbox,\n sessionKey,\n conversationId,\n conversationDir,\n this.options.workingDir,\n sessionScope,\n this.options.vaultManager,\n this.options.bindingStore,\n this.options.provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n this.conversationStates.set(sessionKey, state);\n return state;\n }\n\n async shutdown(timeoutMs = 30_000): Promise<void> {\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + timeoutMs;\n while (this.inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (this.inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${this.inFlightRuns.size} runs still in progress`);\n }\n }\n\n private async resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n ): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n }\n\n private evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of this.conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n this.conversationStates.delete(key);\n }\n }\n\n if (this.conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of this.conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = this.conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n this.conversationStates.delete(idleSessions[i].key);\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"session-runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/session-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,WAAW,EACX,QAAQ,EACR,UAAU,EAGX,MAAM,eAAe,CAAC;AAMvB,OAAO,EAAE,KAAK,WAAW,EAAgB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAgC,MAAM,sBAAsB,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA0B5D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,2EAA2E;IAC3E,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACjF,uBAAuB,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAKD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAEnF","sourcesContent":["import type {\n Bot,\n BotAdapters,\n BotEvent,\n BotHandler,\n PlatformName,\n RunningSession,\n} from \"../adapter.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"../adapters/slack/branch-manager.js\";\nimport { type AgentRunner, createRunner } from \"../agent.js\";\nimport { CommandRegistry, createDefaultCommandRegistry } from \"../commands/index.js\";\nimport type { CommandServices } from \"../commands/index.js\";\nimport { isPrivateConversation } from \"../commands/utils.js\";\nimport * as log from \"../log.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"../session-store.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"../sentry.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"../ui-copy.js\";\nimport * as Sentry from \"@sentry/node\";\nimport { join } from \"path\";\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nexport interface RunSessionOptions {\n event: BotEvent;\n bot: Bot;\n adapters: BotAdapters;\n isEvent?: boolean;\n}\n\nexport interface CreateSessionSandboxOptions {\n conversationId: string;\n platformName: string;\n sessionKey: string;\n}\n\nexport interface SessionRuntimeOptions extends CommandServices {\n /** Override the default command registry (e.g., to add /help, /status). */\n commandRegistry?: CommandRegistry;\n}\n\nexport interface SessionRuntime extends BotHandler {\n runSession(options: RunSessionOptions): Promise<void>;\n createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner>;\n switchConversationModel(conversationId: string, provider: string, model: string): boolean;\n shutdown(timeoutMs?: number): Promise<void>;\n}\n\nconst MAX_SESSIONS = 500;\nconst IDLE_TIMEOUT_MS = 3_600_000;\n\nexport function createSessionRuntime(options: SessionRuntimeOptions): SessionRuntime {\n return new MamaSessionRuntime(options);\n}\n\nclass MamaSessionRuntime implements SessionRuntime {\n private readonly conversationStates = new Map<string, ConversationState>();\n private readonly inFlightRuns = new Set<Promise<void>>();\n private readonly commandRegistry: CommandRegistry;\n private isShuttingDown = false;\n\n constructor(private readonly options: SessionRuntimeOptions) {\n this.options.runtime = this;\n this.commandRegistry = options.commandRegistry ?? createDefaultCommandRegistry();\n }\n\n isRunning(sessionKey: string): boolean {\n const state = this.conversationStates.get(sessionKey);\n return !!state?.running;\n }\n\n getRunningSessions(): RunningSession[] {\n const sessions: RunningSession[] = [];\n for (const [sessionKey, state] of this.conversationStates) {\n if (state.running && state.startedAt) {\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n }\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n }\n\n forceStop(sessionKey: string): void {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n }\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n this.conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n }\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n isEvent?: boolean,\n ): Promise<void> {\n await this.runSession({ event, bot, adapters, isEvent });\n }\n\n async runSession({ event, bot, adapters, isEvent }: RunSessionOptions): Promise<void> {\n const conversationId = event.conversationId;\n if (this.isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledCommand = await this.commandRegistry.handle({\n bot,\n responseCtx: adapters.responseCtx,\n platform: adapters.platform.name as PlatformName,\n platformUserId: event.user,\n conversationId,\n vaultConversationId: event.vaultConversationId,\n sessionKey,\n commandText: event.text,\n privateConversation,\n services: this.options,\n });\n if (handledCommand) return;\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => this.conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await this.getOrCreateState({\n conversationId,\n platformName: adapters.platform.name,\n sessionKey,\n });\n\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n const runPromise = (async () => {\n try {\n const result = await this.runWithInstrumentation(\n adapters,\n { conversationId, sessionKey, isEvent, startedAt: state.startedAt! },\n async () => {\n await adapters.responseCtx.setTyping(true);\n await adapters.responseCtx.setWorking(true);\n const r = await state.runner.run(\n adapters.message,\n adapters.responseCtx,\n adapters.platform,\n );\n await adapters.responseCtx.setWorking(false);\n return r;\n },\n );\n\n if (result?.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size - 1);\n this.evictIdleSessions();\n }\n })();\n\n this.inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n this.inFlightRuns.delete(runPromise);\n }\n }\n\n private async runWithInstrumentation(\n adapters: BotAdapters,\n meta: { conversationId: string; sessionKey: string; isEvent?: boolean; startedAt: number },\n body: () => Promise<{ stopReason: string; errorMessage?: string }>,\n ): Promise<{ stopReason: string; errorMessage?: string } | undefined> {\n const { conversationId, sessionKey, isEvent, startedAt } = meta;\n const { message, platform } = adapters;\n\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size + 1);\n\n return Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () =>\n Sentry.withScope(async (scope) => {\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n const result = await body();\n const durationMs = Date.now() - startedAt;\n const completionAttrs = {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n };\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: completionAttrs,\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, { attributes: completionAttrs });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n return result;\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: platform.name,\n messageId: message.id,\n threadTs: message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n }),\n );\n }\n\n async createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner> {\n const state = await this.getOrCreateState(options);\n return state.runner;\n }\n\n switchConversationModel(conversationId: string, _provider: string, _model: string): boolean {\n for (const [sessionKey, state] of this.conversationStates) {\n if (this.isConversationSession(sessionKey, conversationId) && state.running) {\n return false;\n }\n }\n\n for (const sessionKey of Array.from(this.conversationStates.keys())) {\n if (this.isConversationSession(sessionKey, conversationId)) {\n this.conversationStates.delete(sessionKey);\n }\n }\n log.logInfo(`[${conversationId}] Model switched; cleared cached session runners`);\n return true;\n }\n\n private isConversationSession(sessionKey: string, conversationId: string): boolean {\n return sessionKey === conversationId || sessionKey.startsWith(`${conversationId}:`);\n }\n\n private async getOrCreateState({\n conversationId,\n platformName,\n sessionKey,\n }: CreateSessionSandboxOptions): Promise<ConversationState> {\n const existing = this.conversationStates.get(sessionKey);\n if (existing) {\n existing.lastAccessedAt = Date.now();\n return existing;\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const sessionScope = await this.resolveSessionScope(platformName, conversationDir, sessionKey);\n const state: ConversationState = {\n running: false,\n runner: await createRunner(\n this.options.sandbox,\n sessionKey,\n conversationId,\n conversationDir,\n this.options.workingDir,\n sessionScope,\n this.options.vaultManager,\n this.options.bindingStore,\n this.options.provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n this.conversationStates.set(sessionKey, state);\n return state;\n }\n\n async shutdown(timeoutMs = 30_000): Promise<void> {\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + timeoutMs;\n while (this.inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (this.inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${this.inFlightRuns.size} runs still in progress`);\n }\n }\n\n private async resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n ): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n }\n\n private evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of this.conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n this.conversationStates.delete(key);\n }\n }\n\n if (this.conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of this.conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = this.conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n this.conversationStates.delete(idleSessions[i].key);\n }\n }\n }\n}\n"]}
@@ -19,6 +19,7 @@ class MamaSessionRuntime {
19
19
  this.conversationStates = new Map();
20
20
  this.inFlightRuns = new Set();
21
21
  this.isShuttingDown = false;
22
+ this.options.runtime = this;
22
23
  this.commandRegistry = options.commandRegistry ?? createDefaultCommandRegistry();
23
24
  }
24
25
  isRunning(sessionKey) {
@@ -224,6 +225,23 @@ class MamaSessionRuntime {
224
225
  const state = await this.getOrCreateState(options);
225
226
  return state.runner;
226
227
  }
228
+ switchConversationModel(conversationId, _provider, _model) {
229
+ for (const [sessionKey, state] of this.conversationStates) {
230
+ if (this.isConversationSession(sessionKey, conversationId) && state.running) {
231
+ return false;
232
+ }
233
+ }
234
+ for (const sessionKey of Array.from(this.conversationStates.keys())) {
235
+ if (this.isConversationSession(sessionKey, conversationId)) {
236
+ this.conversationStates.delete(sessionKey);
237
+ }
238
+ }
239
+ log.logInfo(`[${conversationId}] Model switched; cleared cached session runners`);
240
+ return true;
241
+ }
242
+ isConversationSession(sessionKey, conversationId) {
243
+ return sessionKey === conversationId || sessionKey.startsWith(`${conversationId}:`);
244
+ }
227
245
  async getOrCreateState({ conversationId, platformName, sessionKey, }) {
228
246
  const existing = this.conversationStates.get(sessionKey);
229
247
  if (existing) {
@@ -1 +1 @@
1
- {"version":3,"file":"session-runtime.js","sourceRoot":"","sources":["../../src/runtime/session-runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n Bot,\n BotAdapters,\n BotEvent,\n BotHandler,\n PlatformName,\n RunningSession,\n} from \"../adapter.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"../adapters/slack/branch-manager.js\";\nimport { type AgentRunner, createRunner } from \"../agent.js\";\nimport { CommandRegistry, createDefaultCommandRegistry } from \"../commands/index.js\";\nimport type { CommandServices } from \"../commands/index.js\";\nimport { isPrivateConversation } from \"../commands/utils.js\";\nimport * as log from \"../log.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"../session-store.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"../sentry.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"../ui-copy.js\";\nimport * as Sentry from \"@sentry/node\";\nimport { join } from \"path\";\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nexport interface RunSessionOptions {\n event: BotEvent;\n bot: Bot;\n adapters: BotAdapters;\n isEvent?: boolean;\n}\n\nexport interface CreateSessionSandboxOptions {\n conversationId: string;\n platformName: string;\n sessionKey: string;\n}\n\nexport interface SessionRuntimeOptions extends CommandServices {\n /** Override the default command registry (e.g., to add /help, /status). */\n commandRegistry?: CommandRegistry;\n}\n\nexport interface SessionRuntime extends BotHandler {\n runSession(options: RunSessionOptions): Promise<void>;\n createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner>;\n shutdown(timeoutMs?: number): Promise<void>;\n}\n\nconst MAX_SESSIONS = 500;\nconst IDLE_TIMEOUT_MS = 3_600_000;\n\nexport function createSessionRuntime(options: SessionRuntimeOptions): SessionRuntime {\n return new MamaSessionRuntime(options);\n}\n\nclass MamaSessionRuntime implements SessionRuntime {\n private readonly conversationStates = new Map<string, ConversationState>();\n private readonly inFlightRuns = new Set<Promise<void>>();\n private readonly commandRegistry: CommandRegistry;\n private isShuttingDown = false;\n\n constructor(private readonly options: SessionRuntimeOptions) {\n this.commandRegistry = options.commandRegistry ?? createDefaultCommandRegistry();\n }\n\n isRunning(sessionKey: string): boolean {\n const state = this.conversationStates.get(sessionKey);\n return !!state?.running;\n }\n\n getRunningSessions(): RunningSession[] {\n const sessions: RunningSession[] = [];\n for (const [sessionKey, state] of this.conversationStates) {\n if (state.running && state.startedAt) {\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n }\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n }\n\n forceStop(sessionKey: string): void {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n }\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n this.conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n }\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n isEvent?: boolean,\n ): Promise<void> {\n await this.runSession({ event, bot, adapters, isEvent });\n }\n\n async runSession({ event, bot, adapters, isEvent }: RunSessionOptions): Promise<void> {\n const conversationId = event.conversationId;\n if (this.isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledCommand = await this.commandRegistry.handle({\n bot,\n responseCtx: adapters.responseCtx,\n platform: adapters.platform.name as PlatformName,\n platformUserId: event.user,\n conversationId,\n vaultConversationId: event.vaultConversationId,\n sessionKey,\n commandText: event.text,\n privateConversation,\n services: this.options,\n });\n if (handledCommand) return;\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => this.conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await this.getOrCreateState({\n conversationId,\n platformName: adapters.platform.name,\n sessionKey,\n });\n\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n const runPromise = (async () => {\n try {\n const result = await this.runWithInstrumentation(\n adapters,\n { conversationId, sessionKey, isEvent, startedAt: state.startedAt! },\n async () => {\n await adapters.responseCtx.setTyping(true);\n await adapters.responseCtx.setWorking(true);\n const r = await state.runner.run(\n adapters.message,\n adapters.responseCtx,\n adapters.platform,\n );\n await adapters.responseCtx.setWorking(false);\n return r;\n },\n );\n\n if (result?.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size - 1);\n this.evictIdleSessions();\n }\n })();\n\n this.inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n this.inFlightRuns.delete(runPromise);\n }\n }\n\n private async runWithInstrumentation(\n adapters: BotAdapters,\n meta: { conversationId: string; sessionKey: string; isEvent?: boolean; startedAt: number },\n body: () => Promise<{ stopReason: string; errorMessage?: string }>,\n ): Promise<{ stopReason: string; errorMessage?: string } | undefined> {\n const { conversationId, sessionKey, isEvent, startedAt } = meta;\n const { message, platform } = adapters;\n\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size + 1);\n\n return Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () =>\n Sentry.withScope(async (scope) => {\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n const result = await body();\n const durationMs = Date.now() - startedAt;\n const completionAttrs = {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n };\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: completionAttrs,\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, { attributes: completionAttrs });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n return result;\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: platform.name,\n messageId: message.id,\n threadTs: message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n }),\n );\n }\n\n async createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner> {\n const state = await this.getOrCreateState(options);\n return state.runner;\n }\n\n private async getOrCreateState({\n conversationId,\n platformName,\n sessionKey,\n }: CreateSessionSandboxOptions): Promise<ConversationState> {\n const existing = this.conversationStates.get(sessionKey);\n if (existing) {\n existing.lastAccessedAt = Date.now();\n return existing;\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const sessionScope = await this.resolveSessionScope(platformName, conversationDir, sessionKey);\n const state: ConversationState = {\n running: false,\n runner: await createRunner(\n this.options.sandbox,\n sessionKey,\n conversationId,\n conversationDir,\n this.options.workingDir,\n sessionScope,\n this.options.vaultManager,\n this.options.bindingStore,\n this.options.provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n this.conversationStates.set(sessionKey, state);\n return state;\n }\n\n async shutdown(timeoutMs = 30_000): Promise<void> {\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + timeoutMs;\n while (this.inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (this.inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${this.inFlightRuns.size} runs still in progress`);\n }\n }\n\n private async resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n ): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n }\n\n private evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of this.conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n this.conversationStates.delete(key);\n }\n }\n\n if (this.conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of this.conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = this.conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n this.conversationStates.delete(idleSessions[i].key);\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"session-runtime.js","sourceRoot":"","sources":["../../src/runtime/session-runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n Bot,\n BotAdapters,\n BotEvent,\n BotHandler,\n PlatformName,\n RunningSession,\n} from \"../adapter.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"../adapters/slack/branch-manager.js\";\nimport { type AgentRunner, createRunner } from \"../agent.js\";\nimport { CommandRegistry, createDefaultCommandRegistry } from \"../commands/index.js\";\nimport type { CommandServices } from \"../commands/index.js\";\nimport { isPrivateConversation } from \"../commands/utils.js\";\nimport * as log from \"../log.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"../session-store.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"../sentry.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"../ui-copy.js\";\nimport * as Sentry from \"@sentry/node\";\nimport { join } from \"path\";\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nexport interface RunSessionOptions {\n event: BotEvent;\n bot: Bot;\n adapters: BotAdapters;\n isEvent?: boolean;\n}\n\nexport interface CreateSessionSandboxOptions {\n conversationId: string;\n platformName: string;\n sessionKey: string;\n}\n\nexport interface SessionRuntimeOptions extends CommandServices {\n /** Override the default command registry (e.g., to add /help, /status). */\n commandRegistry?: CommandRegistry;\n}\n\nexport interface SessionRuntime extends BotHandler {\n runSession(options: RunSessionOptions): Promise<void>;\n createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner>;\n switchConversationModel(conversationId: string, provider: string, model: string): boolean;\n shutdown(timeoutMs?: number): Promise<void>;\n}\n\nconst MAX_SESSIONS = 500;\nconst IDLE_TIMEOUT_MS = 3_600_000;\n\nexport function createSessionRuntime(options: SessionRuntimeOptions): SessionRuntime {\n return new MamaSessionRuntime(options);\n}\n\nclass MamaSessionRuntime implements SessionRuntime {\n private readonly conversationStates = new Map<string, ConversationState>();\n private readonly inFlightRuns = new Set<Promise<void>>();\n private readonly commandRegistry: CommandRegistry;\n private isShuttingDown = false;\n\n constructor(private readonly options: SessionRuntimeOptions) {\n this.options.runtime = this;\n this.commandRegistry = options.commandRegistry ?? createDefaultCommandRegistry();\n }\n\n isRunning(sessionKey: string): boolean {\n const state = this.conversationStates.get(sessionKey);\n return !!state?.running;\n }\n\n getRunningSessions(): RunningSession[] {\n const sessions: RunningSession[] = [];\n for (const [sessionKey, state] of this.conversationStates) {\n if (state.running && state.startedAt) {\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n }\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n }\n\n forceStop(sessionKey: string): void {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n }\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = this.conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n this.conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n }\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n isEvent?: boolean,\n ): Promise<void> {\n await this.runSession({ event, bot, adapters, isEvent });\n }\n\n async runSession({ event, bot, adapters, isEvent }: RunSessionOptions): Promise<void> {\n const conversationId = event.conversationId;\n if (this.isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledCommand = await this.commandRegistry.handle({\n bot,\n responseCtx: adapters.responseCtx,\n platform: adapters.platform.name as PlatformName,\n platformUserId: event.user,\n conversationId,\n vaultConversationId: event.vaultConversationId,\n sessionKey,\n commandText: event.text,\n privateConversation,\n services: this.options,\n });\n if (handledCommand) return;\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => this.conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await this.getOrCreateState({\n conversationId,\n platformName: adapters.platform.name,\n sessionKey,\n });\n\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n const runPromise = (async () => {\n try {\n const result = await this.runWithInstrumentation(\n adapters,\n { conversationId, sessionKey, isEvent, startedAt: state.startedAt! },\n async () => {\n await adapters.responseCtx.setTyping(true);\n await adapters.responseCtx.setWorking(true);\n const r = await state.runner.run(\n adapters.message,\n adapters.responseCtx,\n adapters.platform,\n );\n await adapters.responseCtx.setWorking(false);\n return r;\n },\n );\n\n if (result?.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size - 1);\n this.evictIdleSessions();\n }\n })();\n\n this.inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n this.inFlightRuns.delete(runPromise);\n }\n }\n\n private async runWithInstrumentation(\n adapters: BotAdapters,\n meta: { conversationId: string; sessionKey: string; isEvent?: boolean; startedAt: number },\n body: () => Promise<{ stopReason: string; errorMessage?: string }>,\n ): Promise<{ stopReason: string; errorMessage?: string } | undefined> {\n const { conversationId, sessionKey, isEvent, startedAt } = meta;\n const { message, platform } = adapters;\n\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", this.inFlightRuns.size + 1);\n\n return Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () =>\n Sentry.withScope(async (scope) => {\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n const result = await body();\n const durationMs = Date.now() - startedAt;\n const completionAttrs = {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n };\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: completionAttrs,\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, { attributes: completionAttrs });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n return result;\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: platform.name,\n messageId: message.id,\n threadTs: message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n }),\n );\n }\n\n async createSessionSandbox(options: CreateSessionSandboxOptions): Promise<AgentRunner> {\n const state = await this.getOrCreateState(options);\n return state.runner;\n }\n\n switchConversationModel(conversationId: string, _provider: string, _model: string): boolean {\n for (const [sessionKey, state] of this.conversationStates) {\n if (this.isConversationSession(sessionKey, conversationId) && state.running) {\n return false;\n }\n }\n\n for (const sessionKey of Array.from(this.conversationStates.keys())) {\n if (this.isConversationSession(sessionKey, conversationId)) {\n this.conversationStates.delete(sessionKey);\n }\n }\n log.logInfo(`[${conversationId}] Model switched; cleared cached session runners`);\n return true;\n }\n\n private isConversationSession(sessionKey: string, conversationId: string): boolean {\n return sessionKey === conversationId || sessionKey.startsWith(`${conversationId}:`);\n }\n\n private async getOrCreateState({\n conversationId,\n platformName,\n sessionKey,\n }: CreateSessionSandboxOptions): Promise<ConversationState> {\n const existing = this.conversationStates.get(sessionKey);\n if (existing) {\n existing.lastAccessedAt = Date.now();\n return existing;\n }\n\n const conversationDir = join(this.options.workingDir, conversationId);\n const sessionScope = await this.resolveSessionScope(platformName, conversationDir, sessionKey);\n const state: ConversationState = {\n running: false,\n runner: await createRunner(\n this.options.sandbox,\n sessionKey,\n conversationId,\n conversationDir,\n this.options.workingDir,\n sessionScope,\n this.options.vaultManager,\n this.options.bindingStore,\n this.options.provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n this.conversationStates.set(sessionKey, state);\n return state;\n }\n\n async shutdown(timeoutMs = 30_000): Promise<void> {\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + timeoutMs;\n while (this.inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (this.inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${this.inFlightRuns.size} runs still in progress`);\n }\n }\n\n private async resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n ): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n }\n\n private evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of this.conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n this.conversationStates.delete(key);\n }\n }\n\n if (this.conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of this.conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = this.conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n this.conversationStates.delete(idleSessions[i].key);\n }\n }\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { SessionManager } from "@mariozechner/pi-coding-agent";
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
2
  export declare class ThreadRootNotFoundError extends Error {
3
3
  constructor(sessionFile: string);
4
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAG/D,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;CACpB;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,CAE/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;AA4HD;;;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, readFileSync, renameSync, rmSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@mariozechner/pi-coding-agent\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.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}\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) 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 return sessionKey.includes(\":\") ? sessionKey.split(\":\").pop()! : 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 sessionDir = getChannelSessionDir(conversationDir);\n\n if (!sessionKey.includes(\":\")) {\n return {\n sessionDir,\n contextFile: resolveManagedSessionFile(sessionDir, conversationDir),\n threadRootMessage: null,\n };\n }\n\n const threadFile = getThreadSessionFile(conversationDir, sessionKey);\n return {\n sessionDir,\n contextFile:\n tryResolveThreadSession(threadFile) ??\n createManagedSessionFileAtPath(threadFile, conversationDir),\n threadRootMessage: null,\n };\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const lines = readFileSync(sessionFile, \"utf-8\").split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = JSON.parse(trimmed) as { type?: string };\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction shouldRecreatePreinitializedSession(sessionFile: string): boolean {\n if (!existsSync(sessionFile)) return false;\n\n try {\n const entries = readFileSync(sessionFile, \"utf-8\")\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => JSON.parse(line) as { type?: string });\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 if (!existsSync(pointerFile)) return null;\n const filename = readFileSync(pointerFile, \"utf-8\").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"]}
1
+ {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE,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;CACpB;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,CAE/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;AA4HD;;;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, readFileSync, renameSync, rmSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.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}\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) 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 return sessionKey.includes(\":\") ? sessionKey.split(\":\").pop()! : 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 sessionDir = getChannelSessionDir(conversationDir);\n\n if (!sessionKey.includes(\":\")) {\n return {\n sessionDir,\n contextFile: resolveManagedSessionFile(sessionDir, conversationDir),\n threadRootMessage: null,\n };\n }\n\n const threadFile = getThreadSessionFile(conversationDir, sessionKey);\n return {\n sessionDir,\n contextFile:\n tryResolveThreadSession(threadFile) ??\n createManagedSessionFileAtPath(threadFile, conversationDir),\n threadRootMessage: null,\n };\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const lines = readFileSync(sessionFile, \"utf-8\").split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = JSON.parse(trimmed) as { type?: string };\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction shouldRecreatePreinitializedSession(sessionFile: string): boolean {\n if (!existsSync(sessionFile)) return false;\n\n try {\n const entries = readFileSync(sessionFile, \"utf-8\")\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => JSON.parse(line) as { type?: string });\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 if (!existsSync(pointerFile)) return null;\n const filename = readFileSync(pointerFile, \"utf-8\").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"]}