@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.11

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 (271) hide show
  1. package/README.md +168 -371
  2. package/dist/adapter.d.ts +36 -12
  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 +12 -7
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +358 -135
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +100 -36
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/shared.d.ts +71 -0
  14. package/dist/adapters/shared.d.ts.map +1 -0
  15. package/dist/adapters/shared.js +168 -0
  16. package/dist/adapters/shared.js.map +1 -0
  17. package/dist/adapters/slack/bot.d.ts +30 -24
  18. package/dist/adapters/slack/bot.d.ts.map +1 -1
  19. package/dist/adapters/slack/bot.js +613 -224
  20. package/dist/adapters/slack/bot.js.map +1 -1
  21. package/dist/adapters/slack/branch-manager.d.ts +22 -0
  22. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  23. package/dist/adapters/slack/branch-manager.js +97 -0
  24. package/dist/adapters/slack/branch-manager.js.map +1 -0
  25. package/dist/adapters/slack/context.d.ts +1 -1
  26. package/dist/adapters/slack/context.d.ts.map +1 -1
  27. package/dist/adapters/slack/context.js +127 -72
  28. package/dist/adapters/slack/context.js.map +1 -1
  29. package/dist/adapters/slack/session.d.ts +3 -0
  30. package/dist/adapters/slack/session.d.ts.map +1 -0
  31. package/dist/adapters/slack/session.js +16 -0
  32. package/dist/adapters/slack/session.js.map +1 -0
  33. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  34. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  35. package/dist/adapters/slack/tools/attach.js.map +1 -1
  36. package/dist/adapters/telegram/bot.d.ts +4 -2
  37. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  38. package/dist/adapters/telegram/bot.js +193 -147
  39. package/dist/adapters/telegram/bot.js.map +1 -1
  40. package/dist/adapters/telegram/context.d.ts.map +1 -1
  41. package/dist/adapters/telegram/context.js +58 -111
  42. package/dist/adapters/telegram/context.js.map +1 -1
  43. package/dist/adapters/telegram/html.d.ts +3 -0
  44. package/dist/adapters/telegram/html.d.ts.map +1 -0
  45. package/dist/adapters/telegram/html.js +98 -0
  46. package/dist/adapters/telegram/html.js.map +1 -0
  47. package/dist/agent.d.ts +9 -13
  48. package/dist/agent.d.ts.map +1 -1
  49. package/dist/agent.js +601 -567
  50. package/dist/agent.js.map +1 -1
  51. package/dist/commands/auto-reply.d.ts +16 -0
  52. package/dist/commands/auto-reply.d.ts.map +1 -0
  53. package/dist/commands/auto-reply.js +69 -0
  54. package/dist/commands/auto-reply.js.map +1 -0
  55. package/dist/commands/index.d.ts +5 -0
  56. package/dist/commands/index.d.ts.map +1 -0
  57. package/dist/commands/index.js +19 -0
  58. package/dist/commands/index.js.map +1 -0
  59. package/dist/commands/login.d.ts +5 -0
  60. package/dist/commands/login.d.ts.map +1 -0
  61. package/dist/commands/login.js +76 -0
  62. package/dist/commands/login.js.map +1 -0
  63. package/dist/commands/model.d.ts +14 -0
  64. package/dist/commands/model.d.ts.map +1 -0
  65. package/dist/commands/model.js +112 -0
  66. package/dist/commands/model.js.map +1 -0
  67. package/dist/commands/new.d.ts +9 -0
  68. package/dist/commands/new.d.ts.map +1 -0
  69. package/dist/commands/new.js +28 -0
  70. package/dist/commands/new.js.map +1 -0
  71. package/dist/commands/registry.d.ts +7 -0
  72. package/dist/commands/registry.d.ts.map +1 -0
  73. package/dist/commands/registry.js +14 -0
  74. package/dist/commands/registry.js.map +1 -0
  75. package/dist/commands/sandbox.d.ts +10 -0
  76. package/dist/commands/sandbox.d.ts.map +1 -0
  77. package/dist/commands/sandbox.js +88 -0
  78. package/dist/commands/sandbox.js.map +1 -0
  79. package/dist/commands/session-view.d.ts +5 -0
  80. package/dist/commands/session-view.d.ts.map +1 -0
  81. package/dist/commands/session-view.js +62 -0
  82. package/dist/commands/session-view.js.map +1 -0
  83. package/dist/commands/types.d.ts +41 -0
  84. package/dist/commands/types.d.ts.map +1 -0
  85. package/dist/commands/types.js +2 -0
  86. package/dist/commands/types.js.map +1 -0
  87. package/dist/commands/utils.d.ts +8 -0
  88. package/dist/commands/utils.d.ts.map +1 -0
  89. package/dist/commands/utils.js +14 -0
  90. package/dist/commands/utils.js.map +1 -0
  91. package/dist/config.d.ts +49 -30
  92. package/dist/config.d.ts.map +1 -1
  93. package/dist/config.js +313 -75
  94. package/dist/config.js.map +1 -1
  95. package/dist/context.d.ts +10 -42
  96. package/dist/context.d.ts.map +1 -1
  97. package/dist/context.js +14 -127
  98. package/dist/context.js.map +1 -1
  99. package/dist/events.d.ts +13 -6
  100. package/dist/events.d.ts.map +1 -1
  101. package/dist/events.js +118 -64
  102. package/dist/events.js.map +1 -1
  103. package/dist/execution-resolver.d.ts +9 -5
  104. package/dist/execution-resolver.d.ts.map +1 -1
  105. package/dist/execution-resolver.js +82 -18
  106. package/dist/execution-resolver.js.map +1 -1
  107. package/dist/file-guards.d.ts +6 -0
  108. package/dist/file-guards.d.ts.map +1 -0
  109. package/dist/file-guards.js +48 -0
  110. package/dist/file-guards.js.map +1 -0
  111. package/dist/fs-atomic.d.ts +10 -0
  112. package/dist/fs-atomic.d.ts.map +1 -0
  113. package/dist/fs-atomic.js +45 -0
  114. package/dist/fs-atomic.js.map +1 -0
  115. package/dist/index.d.ts +7 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +4 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/instrument.d.ts.map +1 -1
  120. package/dist/instrument.js +4 -11
  121. package/dist/instrument.js.map +1 -1
  122. package/dist/log.d.ts +1 -5
  123. package/dist/log.d.ts.map +1 -1
  124. package/dist/log.js +13 -38
  125. package/dist/log.js.map +1 -1
  126. package/dist/{login.d.ts → login/index.d.ts} +16 -4
  127. package/dist/login/index.d.ts.map +1 -0
  128. package/dist/{login.js → login/index.js} +55 -17
  129. package/dist/login/index.js.map +1 -0
  130. package/dist/{link-server.d.ts → login/portal.d.ts} +7 -4
  131. package/dist/login/portal.d.ts.map +1 -0
  132. package/dist/login/portal.js +1453 -0
  133. package/dist/login/portal.js.map +1 -0
  134. package/dist/{link-token.d.ts → login/session.d.ts} +4 -3
  135. package/dist/login/session.d.ts.map +1 -0
  136. package/dist/{link-token.js → login/session.js} +1 -1
  137. package/dist/login/session.js.map +1 -0
  138. package/dist/main.d.ts.map +1 -1
  139. package/dist/main.js +151 -373
  140. package/dist/main.js.map +1 -1
  141. package/dist/provisioner.d.ts +42 -52
  142. package/dist/provisioner.d.ts.map +1 -1
  143. package/dist/provisioner.js +256 -111
  144. package/dist/provisioner.js.map +1 -1
  145. package/dist/runtime/conversation-orchestrator.d.ts +42 -0
  146. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  147. package/dist/runtime/conversation-orchestrator.js +150 -0
  148. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  149. package/dist/runtime/index.d.ts +2 -0
  150. package/dist/runtime/index.d.ts.map +1 -0
  151. package/dist/runtime/index.js +2 -0
  152. package/dist/runtime/index.js.map +1 -0
  153. package/dist/runtime/session-runtime.d.ts +27 -0
  154. package/dist/runtime/session-runtime.d.ts.map +1 -0
  155. package/dist/runtime/session-runtime.js +211 -0
  156. package/dist/runtime/session-runtime.js.map +1 -0
  157. package/dist/sandbox/cloudflare.d.ts +15 -0
  158. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  159. package/dist/sandbox/cloudflare.js +137 -0
  160. package/dist/sandbox/cloudflare.js.map +1 -0
  161. package/dist/sandbox/container.d.ts +2 -1
  162. package/dist/sandbox/container.d.ts.map +1 -1
  163. package/dist/sandbox/container.js +5 -1
  164. package/dist/sandbox/container.js.map +1 -1
  165. package/dist/sandbox/firecracker.d.ts +2 -1
  166. package/dist/sandbox/firecracker.d.ts.map +1 -1
  167. package/dist/sandbox/firecracker.js +6 -0
  168. package/dist/sandbox/firecracker.js.map +1 -1
  169. package/dist/sandbox/host.d.ts +2 -3
  170. package/dist/sandbox/host.d.ts.map +1 -1
  171. package/dist/sandbox/host.js +5 -5
  172. package/dist/sandbox/host.js.map +1 -1
  173. package/dist/sandbox/index.d.ts +6 -4
  174. package/dist/sandbox/index.d.ts.map +1 -1
  175. package/dist/sandbox/index.js +9 -6
  176. package/dist/sandbox/index.js.map +1 -1
  177. package/dist/sandbox/path-context.d.ts +4 -0
  178. package/dist/sandbox/path-context.d.ts.map +1 -0
  179. package/dist/sandbox/path-context.js +20 -0
  180. package/dist/sandbox/path-context.js.map +1 -0
  181. package/dist/sandbox/types.d.ts +17 -1
  182. package/dist/sandbox/types.d.ts.map +1 -1
  183. package/dist/sandbox/types.js.map +1 -1
  184. package/dist/sentry.d.ts +1 -1
  185. package/dist/sentry.d.ts.map +1 -1
  186. package/dist/sentry.js +4 -2
  187. package/dist/sentry.js.map +1 -1
  188. package/dist/session-policy.d.ts +13 -0
  189. package/dist/session-policy.d.ts.map +1 -0
  190. package/dist/session-policy.js +23 -0
  191. package/dist/session-policy.js.map +1 -0
  192. package/dist/session-store.d.ts +34 -3
  193. package/dist/session-store.d.ts.map +1 -1
  194. package/dist/session-store.js +184 -22
  195. package/dist/session-store.js.map +1 -1
  196. package/dist/session-view/command.d.ts +5 -0
  197. package/dist/session-view/command.d.ts.map +1 -0
  198. package/dist/session-view/command.js +11 -0
  199. package/dist/session-view/command.js.map +1 -0
  200. package/dist/session-view/portal.d.ts +16 -0
  201. package/dist/session-view/portal.d.ts.map +1 -0
  202. package/dist/session-view/portal.js +1742 -0
  203. package/dist/session-view/portal.js.map +1 -0
  204. package/dist/session-view/service.d.ts +34 -0
  205. package/dist/session-view/service.d.ts.map +1 -0
  206. package/dist/session-view/service.js +427 -0
  207. package/dist/session-view/service.js.map +1 -0
  208. package/dist/session-view/store.d.ts +18 -0
  209. package/dist/session-view/store.d.ts.map +1 -0
  210. package/dist/session-view/store.js +39 -0
  211. package/dist/session-view/store.js.map +1 -0
  212. package/dist/store.d.ts +3 -6
  213. package/dist/store.d.ts.map +1 -1
  214. package/dist/store.js +22 -48
  215. package/dist/store.js.map +1 -1
  216. package/dist/tool-diagnostics.d.ts +2 -0
  217. package/dist/tool-diagnostics.d.ts.map +1 -0
  218. package/dist/tool-diagnostics.js +7 -0
  219. package/dist/tool-diagnostics.js.map +1 -0
  220. package/dist/tools/bash.d.ts +1 -1
  221. package/dist/tools/bash.d.ts.map +1 -1
  222. package/dist/tools/bash.js.map +1 -1
  223. package/dist/tools/edit.d.ts +1 -1
  224. package/dist/tools/edit.d.ts.map +1 -1
  225. package/dist/tools/edit.js.map +1 -1
  226. package/dist/tools/event.d.ts +43 -2
  227. package/dist/tools/event.d.ts.map +1 -1
  228. package/dist/tools/event.js +48 -13
  229. package/dist/tools/event.js.map +1 -1
  230. package/dist/tools/index.d.ts +2 -1
  231. package/dist/tools/index.d.ts.map +1 -1
  232. package/dist/tools/index.js +3 -3
  233. package/dist/tools/index.js.map +1 -1
  234. package/dist/tools/read.d.ts +1 -1
  235. package/dist/tools/read.d.ts.map +1 -1
  236. package/dist/tools/read.js.map +1 -1
  237. package/dist/tools/write.d.ts +1 -1
  238. package/dist/tools/write.d.ts.map +1 -1
  239. package/dist/tools/write.js.map +1 -1
  240. package/dist/trigger.d.ts +31 -0
  241. package/dist/trigger.d.ts.map +1 -0
  242. package/dist/trigger.js +98 -0
  243. package/dist/trigger.js.map +1 -0
  244. package/dist/ui-copy.d.ts +1 -0
  245. package/dist/ui-copy.d.ts.map +1 -1
  246. package/dist/ui-copy.js +3 -0
  247. package/dist/ui-copy.js.map +1 -1
  248. package/dist/vault-routing.d.ts +1 -7
  249. package/dist/vault-routing.d.ts.map +1 -1
  250. package/dist/vault-routing.js +6 -48
  251. package/dist/vault-routing.js.map +1 -1
  252. package/dist/vault.d.ts +21 -55
  253. package/dist/vault.d.ts.map +1 -1
  254. package/dist/vault.js +144 -263
  255. package/dist/vault.js.map +1 -1
  256. package/package.json +12 -10
  257. package/dist/bindings.d.ts +0 -63
  258. package/dist/bindings.d.ts.map +0 -1
  259. package/dist/bindings.js +0 -94
  260. package/dist/bindings.js.map +0 -1
  261. package/dist/link-server.d.ts.map +0 -1
  262. package/dist/link-server.js +0 -839
  263. package/dist/link-server.js.map +0 -1
  264. package/dist/link-token.d.ts.map +0 -1
  265. package/dist/link-token.js.map +0 -1
  266. package/dist/login.d.ts.map +0 -1
  267. package/dist/login.js.map +0 -1
  268. package/dist/vault.test.d.ts +0 -2
  269. package/dist/vault.test.d.ts.map +0 -1
  270. package/dist/vault.test.js +0 -67
  271. package/dist/vault.test.js.map +0 -1
@@ -1,33 +1,42 @@
1
1
  import { execFile } from "child_process";
2
+ import { createHash } from "crypto";
3
+ import { readFileSync, statSync } from "fs";
2
4
  import { promisify } from "util";
3
5
  import * as log from "./log.js";
4
6
  const execFileAsync = promisify(execFile);
5
- // ── DockerContainerManager ─────────────────────────────────────────────────────
6
- /**
7
- * Manages the lifecycle of per-user Docker containers.
8
- *
9
- * Tracks each container's status in memory (running / stopped / missing).
10
- * State is always verified against Docker on provision(), so in-memory state
11
- * stays accurate without polling.
12
- */
7
+ function isDockerNotFoundError(err) {
8
+ if (!err || typeof err !== "object")
9
+ return false;
10
+ const stderr = err.stderr;
11
+ const message = err.message;
12
+ const haystack = `${typeof stderr === "string" ? stderr : ""}\n${typeof message === "string" ? message : ""}`.toLowerCase();
13
+ return (haystack.includes("no such network") ||
14
+ haystack.includes("no such container") ||
15
+ haystack.includes("no such object") ||
16
+ haystack.includes("network not found") ||
17
+ /network [^\n]+ not found/.test(haystack) ||
18
+ /error: no such [^\n]+/.test(haystack));
19
+ }
13
20
  export class DockerContainerManager {
14
21
  static { this.MANAGED_LABEL = "mama.managed=true"; }
15
22
  static { this.IMAGE_MODE_LABEL = "mama.sandbox=image"; }
16
23
  static { this.VAULT_ID_LABEL_KEY = "mama.vault-id"; }
17
- constructor(image, workspaceDir, execFileImpl = execFileAsync) {
24
+ static { this.CONVERSATION_ID_LABEL_KEY = "mama.conversation-id"; }
25
+ static { this.MOUNT_SIGNATURE_LABEL_KEY = "mama.mount-signature"; }
26
+ constructor(image, options = {}) {
18
27
  this.image = image;
19
- this.workspaceDir = workspaceDir;
20
- this.execFileImpl = execFileImpl;
21
28
  this.state = new Map();
22
- /**
23
- * In-flight provision() calls per vaultId. A concurrent second call for the
24
- * same user piggybacks on the first docker start/run instead of racing —
25
- * without this, two parallel messages from one user could produce duplicate
26
- * containers or conflict on docker run.
27
- */
28
29
  this.inflight = new Map();
30
+ this.boostedKeys = new Set();
31
+ if (typeof options === "function") {
32
+ this.execFileImpl = options;
33
+ }
34
+ else {
35
+ this.limits = options.limits;
36
+ this.boostLimits = options.boostLimits;
37
+ this.execFileImpl = options.execFileImpl ?? execFileAsync;
38
+ }
29
39
  }
30
- /** Sanitize an identifier segment for use in vault keys and container names. */
31
40
  static sanitizeSegment(value) {
32
41
  const sanitized = value
33
42
  .toLowerCase()
@@ -35,45 +44,32 @@ export class DockerContainerManager {
35
44
  .replace(/^-+|-+$/g, "");
36
45
  return sanitized || "unknown";
37
46
  }
38
- /**
39
- * Deterministic vault key for a platform user.
40
- * e.g. ("slack", "U04ABC") → "slack-u04abc"
41
- */
42
- static vaultId(platform, platformUserId) {
43
- return `${DockerContainerManager.sanitizeSegment(platform)}-${DockerContainerManager.sanitizeSegment(platformUserId)}`;
44
- }
45
- /** Deterministic container name for a vault-backed user sandbox. */
46
- static containerName(vaultId) {
47
- return `mama-sandbox-${vaultId}`;
48
- }
49
- /**
50
- * Ensure a container exists and is running for the given vaultId.
51
- * Always inspects the actual Docker state, then acts accordingly:
52
- * - running → no-op
53
- * - stopped → docker start
54
- * - missing → docker run
55
- *
56
- * Returns the container name.
57
- */
58
- async provision(vaultId, options = {}) {
59
- const existing = this.inflight.get(vaultId);
47
+ static containerName(containerKey) {
48
+ return `mama-sandbox-${containerKey}`;
49
+ }
50
+ static networkName(containerKey) {
51
+ return `mama-sandbox-net-${containerKey}`;
52
+ }
53
+ async provision(containerKey, options = {}) {
54
+ const existing = this.inflight.get(containerKey);
60
55
  if (existing)
61
56
  return existing;
62
- const pending = this.provisionInner(vaultId, options).finally(() => {
63
- this.inflight.delete(vaultId);
57
+ const pending = this.provisionInner(containerKey, options).finally(() => {
58
+ this.inflight.delete(containerKey);
64
59
  });
65
- this.inflight.set(vaultId, pending);
60
+ this.inflight.set(containerKey, pending);
66
61
  return pending;
67
62
  }
68
- async provisionInner(vaultId, options) {
69
- const containerName = options.containerName ?? DockerContainerManager.containerName(vaultId);
63
+ async provisionInner(containerKey, options) {
64
+ const containerName = options.containerName ?? DockerContainerManager.containerName(containerKey);
70
65
  const mounts = options.mounts ?? [];
71
66
  const status = await this.inspectStatus(containerName);
72
67
  try {
73
- if (status !== "missing" && (await this.hasBindMountDrift(containerName, mounts))) {
74
- log.logInfo(`Container ${containerName} mounts changed; recreating container`);
68
+ if (status !== "missing" &&
69
+ (await this.hasRuntimeDrift(containerKey, containerName, mounts))) {
70
+ log.logInfo(`Container ${containerName} configuration changed; recreating container`);
75
71
  await this.execFileImpl("docker", ["rm", "-f", containerName]);
76
- await this.runContainer(vaultId, containerName, mounts);
72
+ await this.runContainer(containerKey, containerName, mounts, options);
77
73
  log.logInfo(`Container ${containerName} recreated`);
78
74
  }
79
75
  else if (status === "running") {
@@ -84,65 +80,75 @@ export class DockerContainerManager {
84
80
  log.logInfo(`Container ${containerName} started`);
85
81
  }
86
82
  else {
87
- await this.runContainer(vaultId, containerName, mounts);
83
+ await this.runContainer(containerKey, containerName, mounts, options);
88
84
  log.logInfo(`Container ${containerName} created`);
89
85
  }
90
86
  }
91
87
  catch (err) {
92
- // Drop cached state so the next provision() re-inspects Docker cleanly
93
- // and stopIdle doesn't keep trying to stop a container that never
94
- // became running. We deliberately don't bump lastUsed here.
95
- this.state.delete(vaultId);
88
+ this.state.delete(containerKey);
96
89
  throw err;
97
90
  }
98
- this.setState(vaultId, "running", containerName);
91
+ this.setState(containerKey, "running", containerName);
92
+ await this.applyResourceLimits(containerKey, containerName);
99
93
  return containerName;
100
94
  }
101
- /**
102
- * Stop a running container (docker stop). Container is preserved and can be
103
- * restarted via provision(). Intended for idle lifecycle management.
104
- */
105
- async stop(vaultId) {
106
- const containerName = this.getContainerName(vaultId);
95
+ async boost(containerKey) {
96
+ if (!this.boostLimits?.cpus && !this.boostLimits?.memory) {
97
+ return this.getLimitStatus(containerKey);
98
+ }
99
+ this.boostedKeys.add(containerKey);
100
+ const state = this.state.get(containerKey);
101
+ if (state?.status === "running") {
102
+ await this.applyResourceLimits(containerKey, state.containerName);
103
+ }
104
+ return this.getLimitStatus(containerKey);
105
+ }
106
+ getLimitStatus(containerKey) {
107
+ const boosted = this.boostedKeys.has(containerKey);
108
+ return { limits: this.effectiveLimits(containerKey), boosted };
109
+ }
110
+ getDefaultLimits() {
111
+ return this.limits;
112
+ }
113
+ getBoostLimits() {
114
+ return this.boostLimits;
115
+ }
116
+ async stop(containerKey) {
117
+ const containerName = this.getContainerName(containerKey);
107
118
  try {
108
119
  await this.execFileImpl("docker", ["stop", containerName]);
109
- this.setState(vaultId, "stopped", containerName);
120
+ this.setState(containerKey, "stopped", containerName);
121
+ this.boostedKeys.delete(containerKey);
110
122
  log.logInfo(`Container ${containerName} stopped (idle)`);
111
123
  }
112
124
  catch (err) {
113
125
  log.logWarning(`Failed to stop container ${containerName}`, err instanceof Error ? err.message : String(err));
114
126
  }
115
127
  }
116
- /** Stop and remove a container permanently (e.g. on vault revocation). */
117
- async remove(vaultId) {
118
- const containerName = this.getContainerName(vaultId);
128
+ async remove(containerKey) {
129
+ const containerName = this.getContainerName(containerKey);
130
+ const networkName = DockerContainerManager.networkName(containerKey);
131
+ await this.forceRemoveContainer(containerName, `Container ${containerName} removed`, `Failed to remove container ${containerName}`);
119
132
  try {
120
- await this.execFileImpl("docker", ["rm", "-f", containerName]);
121
- this.state.delete(vaultId);
122
- log.logInfo(`Container ${containerName} removed`);
133
+ await this.execFileImpl("docker", ["network", "rm", networkName]);
134
+ log.logInfo(`Network ${networkName} removed`);
123
135
  }
124
136
  catch (err) {
125
- log.logWarning(`Failed to remove container ${containerName}`, err instanceof Error ? err.message : String(err));
137
+ log.logWarning(`Failed to remove network ${networkName}`, err instanceof Error ? err.message : String(err));
126
138
  }
139
+ this.state.delete(containerKey);
140
+ this.boostedKeys.delete(containerKey);
127
141
  }
128
- /**
129
- * Stop all containers that have been idle for longer than maxIdleMs.
130
- * Idle time is measured from the last provision() call.
131
- */
132
142
  async stopIdle(maxIdleMs) {
133
143
  const now = Date.now();
134
144
  const toStop = [];
135
- for (const [vaultId, containerState] of this.state) {
145
+ for (const [containerKey, containerState] of this.state) {
136
146
  if (containerState.status === "running" && now - containerState.lastUsed > maxIdleMs) {
137
- toStop.push(vaultId);
147
+ toStop.push(containerKey);
138
148
  }
139
149
  }
140
- await Promise.all(toStop.map((vaultId) => this.stop(vaultId)));
150
+ await Promise.all(toStop.map((containerKey) => this.stop(containerKey)));
141
151
  }
142
- /**
143
- * Rebuild in-memory state from existing Docker containers managed by mama image mode.
144
- * Supports both new labeled containers and legacy name-prefixed containers.
145
- */
146
152
  async reconcile() {
147
153
  const discovered = new Set();
148
154
  const labeledNames = await this.listContainerNamesByLabel();
@@ -152,28 +158,38 @@ export class DockerContainerManager {
152
158
  for (const name of legacyNames)
153
159
  discovered.add(name);
154
160
  this.state.clear();
155
- for (const containerName of discovered) {
156
- const details = await this.inspectContainerDetails(containerName);
161
+ const inspected = await Promise.all(Array.from(discovered).map(async (containerName) => ({
162
+ containerName,
163
+ details: await this.inspectContainerDetails(containerName),
164
+ })));
165
+ const legacyRemovals = [];
166
+ for (const { containerName, details } of inspected) {
157
167
  if (!details)
158
168
  continue;
159
- const vaultId = details.vaultId || this.vaultIdFromContainerName(containerName);
160
- if (!vaultId) {
161
- log.logWarning(`Skipping unmanaged-style container without vault id`, containerName);
169
+ if (!details.conversationId) {
170
+ legacyRemovals.push(this.removeLegacyContainer(containerName));
171
+ continue;
172
+ }
173
+ const containerKey = this.containerKeyFromContainerName(containerName);
174
+ if (!containerKey) {
175
+ log.logWarning(`Skipping unmanaged-style container without container key`, containerName);
162
176
  continue;
163
177
  }
164
178
  const status = details.running ? "running" : "stopped";
165
179
  const lastUsed = details.startedAtMs ?? Date.now();
166
- this.state.set(vaultId, { status, lastUsed, containerName });
180
+ this.state.set(containerKey, { status, lastUsed, containerName });
167
181
  }
182
+ await Promise.all(legacyRemovals);
168
183
  const running = Array.from(this.state.values()).filter((s) => s.status === "running").length;
169
184
  const stopped = this.state.size - running;
170
185
  log.logInfo(`Reconciled ${this.state.size} managed containers (running=${running}, stopped=${stopped})`);
171
186
  }
172
- setState(vaultId, status, containerName) {
173
- this.state.set(vaultId, { status, lastUsed: Date.now(), containerName });
187
+ setState(containerKey, status, containerName) {
188
+ this.state.set(containerKey, { status, lastUsed: Date.now(), containerName });
174
189
  }
175
- getContainerName(vaultId) {
176
- return this.state.get(vaultId)?.containerName ?? DockerContainerManager.containerName(vaultId);
190
+ getContainerName(containerKey) {
191
+ return (this.state.get(containerKey)?.containerName ??
192
+ DockerContainerManager.containerName(containerKey));
177
193
  }
178
194
  mountArgs(mounts) {
179
195
  return mounts.flatMap((mount) => ["-v", this.toBindSpec(mount)]);
@@ -181,36 +197,82 @@ export class DockerContainerManager {
181
197
  toBindSpec(mount) {
182
198
  return `${mount.source}:${mount.target}`;
183
199
  }
184
- async runContainer(vaultId, containerName, mounts) {
200
+ async runContainer(containerKey, containerName, mounts, options) {
201
+ const networkName = await this.ensureNetwork(containerKey);
185
202
  log.logInfo(`Creating container ${containerName} from image ${this.image}`);
186
- await this.execFileImpl("docker", [
187
- "run",
188
- "-d",
189
- "--name",
190
- containerName,
203
+ const labels = [
191
204
  "--label",
192
205
  DockerContainerManager.MANAGED_LABEL,
193
206
  "--label",
194
207
  DockerContainerManager.IMAGE_MODE_LABEL,
195
208
  "--label",
196
- `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${vaultId}`,
197
- "-v",
198
- `${this.workspaceDir}:/workspace`,
209
+ `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,
210
+ ];
211
+ if (options.conversationId) {
212
+ labels.push("--label", `${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}=${options.conversationId}`);
213
+ }
214
+ if (mounts.length > 0) {
215
+ labels.push("--label", `${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}=${this.mountSignature(mounts)}`);
216
+ }
217
+ await this.execFileImpl("docker", [
218
+ "run",
219
+ "-d",
220
+ "--name",
221
+ containerName,
222
+ "--network",
223
+ networkName,
224
+ ...labels,
225
+ ...this.resourceLimitArgs(this.effectiveLimits(containerKey)),
199
226
  ...this.mountArgs(mounts),
200
227
  this.image,
201
228
  "sleep",
202
229
  "infinity",
203
230
  ]);
204
231
  }
232
+ effectiveLimits(containerKey) {
233
+ if (!this.boostedKeys.has(containerKey))
234
+ return this.limits;
235
+ return { ...this.limits, ...this.boostLimits };
236
+ }
237
+ resourceLimitArgs(limits) {
238
+ const args = [];
239
+ if (limits?.cpus)
240
+ args.push("--cpus", limits.cpus);
241
+ if (limits?.memory)
242
+ args.push("--memory", limits.memory);
243
+ return args;
244
+ }
245
+ async applyResourceLimits(containerKey, containerName) {
246
+ const limitArgs = this.resourceLimitArgs(this.effectiveLimits(containerKey));
247
+ if (limitArgs.length === 0)
248
+ return;
249
+ const args = ["update", ...limitArgs, containerName];
250
+ try {
251
+ await this.execFileImpl("docker", args);
252
+ }
253
+ catch (err) {
254
+ log.logWarning(`Failed to apply resource limits to container ${containerName}`, err instanceof Error ? err.message : String(err));
255
+ }
256
+ }
257
+ async hasRuntimeDrift(containerKey, containerName, mounts) {
258
+ if (await this.hasBindMountDrift(containerName, mounts)) {
259
+ return true;
260
+ }
261
+ if (await this.hasMountSignatureDrift(containerName, mounts)) {
262
+ return true;
263
+ }
264
+ return this.hasNetworkModeDrift(containerKey, containerName);
265
+ }
205
266
  async hasBindMountDrift(containerName, mounts) {
206
267
  const expected = this.expectedBinds(mounts);
207
268
  const actual = await this.inspectBindMounts(containerName);
208
269
  return !this.sameBinds(expected, actual);
209
270
  }
210
271
  expectedBinds(mounts) {
211
- return [`${this.workspaceDir}:/workspace`, ...mounts.map((mount) => this.toBindSpec(mount))]
272
+ return mounts
273
+ .map((mount) => this.toBindSpec(mount))
212
274
  .slice()
213
- .sort();
275
+ .toSorted();
214
276
  }
215
277
  sameBinds(expected, actual) {
216
278
  if (expected.length !== actual.length) {
@@ -218,6 +280,41 @@ export class DockerContainerManager {
218
280
  }
219
281
  return expected.every((bind, index) => bind === actual[index]);
220
282
  }
283
+ async hasMountSignatureDrift(containerName, mounts) {
284
+ if (mounts.length === 0)
285
+ return false;
286
+ const expected = this.mountSignature(mounts);
287
+ const { stdout } = await this.execFileImpl("docker", [
288
+ "inspect",
289
+ "-f",
290
+ `{{index .Config.Labels "${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}"}}`,
291
+ containerName,
292
+ ]);
293
+ const actual = this.normalizeDockerValue(stdout.trim());
294
+ return actual !== expected;
295
+ }
296
+ mountSignature(mounts) {
297
+ const payload = mounts
298
+ .map((mount) => ({
299
+ source: mount.source,
300
+ target: mount.target,
301
+ fingerprint: this.mountSourceFingerprint(mount.source),
302
+ }))
303
+ .toSorted((left, right) => `${left.target}\0${left.source}`.localeCompare(`${right.target}\0${right.source}`));
304
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
305
+ }
306
+ mountSourceFingerprint(source) {
307
+ try {
308
+ const stat = statSync(source);
309
+ if (stat.isFile()) {
310
+ return createHash("sha256").update(readFileSync(source)).digest("hex");
311
+ }
312
+ return `${stat.isDirectory() ? "dir" : "other"}:${stat.size}:${stat.mtimeMs}`;
313
+ }
314
+ catch {
315
+ return "missing";
316
+ }
317
+ }
221
318
  async inspectBindMounts(containerName) {
222
319
  const { stdout } = await this.execFileImpl("docker", [
223
320
  "inspect",
@@ -233,7 +330,42 @@ export class DockerContainerManager {
233
330
  if (!Array.isArray(parsed) || parsed.some((bind) => typeof bind !== "string")) {
234
331
  throw new Error(`Unexpected docker bind mount payload for container "${containerName}"`);
235
332
  }
236
- return [...parsed].sort();
333
+ return [...parsed].toSorted();
334
+ }
335
+ async hasNetworkModeDrift(containerKey, containerName) {
336
+ const expected = DockerContainerManager.networkName(containerKey);
337
+ const { stdout } = await this.execFileImpl("docker", [
338
+ "inspect",
339
+ "-f",
340
+ "{{.HostConfig.NetworkMode}}",
341
+ containerName,
342
+ ]);
343
+ return stdout.trim() !== expected;
344
+ }
345
+ async ensureNetwork(containerKey) {
346
+ const networkName = DockerContainerManager.networkName(containerKey);
347
+ try {
348
+ await this.execFileImpl("docker", ["network", "inspect", networkName]);
349
+ return networkName;
350
+ }
351
+ catch (err) {
352
+ if (!isDockerNotFoundError(err))
353
+ throw err;
354
+ }
355
+ await this.execFileImpl("docker", [
356
+ "network",
357
+ "create",
358
+ "--driver",
359
+ "bridge",
360
+ "--label",
361
+ DockerContainerManager.MANAGED_LABEL,
362
+ "--label",
363
+ DockerContainerManager.IMAGE_MODE_LABEL,
364
+ "--label",
365
+ `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,
366
+ networkName,
367
+ ]);
368
+ return networkName;
237
369
  }
238
370
  async inspectStatus(containerName) {
239
371
  try {
@@ -245,8 +377,10 @@ export class DockerContainerManager {
245
377
  ]);
246
378
  return stdout.trim() === "true" ? "running" : "stopped";
247
379
  }
248
- catch {
249
- return "missing";
380
+ catch (err) {
381
+ if (isDockerNotFoundError(err))
382
+ return "missing";
383
+ throw err;
250
384
  }
251
385
  }
252
386
  async listContainerNamesByLabel() {
@@ -296,14 +430,15 @@ export class DockerContainerManager {
296
430
  const { stdout } = await this.execFileImpl("docker", [
297
431
  "inspect",
298
432
  "-f",
299
- `{{.State.Running}}\t{{.State.StartedAt}}\t{{index .Config.Labels "${DockerContainerManager.VAULT_ID_LABEL_KEY}"}}`,
433
+ `{{.State.Running}}\t{{.State.StartedAt}}\t{{index .Config.Labels "${DockerContainerManager.VAULT_ID_LABEL_KEY}"}}\t{{index .Config.Labels "${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}"}}`,
300
434
  containerName,
301
435
  ]);
302
- const [runningRaw, startedAtRaw, vaultIdRaw] = stdout.trim().split("\t");
436
+ const [runningRaw, startedAtRaw, vaultIdRaw, conversationIdRaw] = stdout.trim().split("\t");
303
437
  const running = runningRaw === "true";
304
438
  const startedAtMs = this.parseDockerTimestamp(startedAtRaw);
305
439
  const vaultId = this.normalizeDockerValue(vaultIdRaw);
306
- return { running, startedAtMs, vaultId };
440
+ const conversationId = this.normalizeDockerValue(conversationIdRaw);
441
+ return { running, startedAtMs, vaultId, conversationId };
307
442
  }
308
443
  catch (err) {
309
444
  log.logWarning(`Failed to inspect container ${containerName} during reconcile`, err instanceof Error ? err.message : String(err));
@@ -323,14 +458,24 @@ export class DockerContainerManager {
323
458
  const parsed = Date.parse(normalized);
324
459
  return Number.isNaN(parsed) ? undefined : parsed;
325
460
  }
326
- vaultIdFromContainerName(containerName) {
461
+ containerKeyFromContainerName(containerName) {
327
462
  const prefix = DockerContainerManager.containerName("");
328
463
  if (!containerName.startsWith(prefix))
329
464
  return undefined;
330
- const vaultId = containerName.slice(prefix.length);
331
- return vaultId.length > 0 ? vaultId : undefined;
465
+ const containerKey = containerName.slice(prefix.length);
466
+ return containerKey.length > 0 ? containerKey : undefined;
467
+ }
468
+ async forceRemoveContainer(containerName, successLog, failureLog) {
469
+ try {
470
+ await this.execFileImpl("docker", ["rm", "-f", containerName]);
471
+ log.logInfo(successLog);
472
+ }
473
+ catch (err) {
474
+ log.logWarning(failureLog, err instanceof Error ? err.message : String(err));
475
+ }
476
+ }
477
+ async removeLegacyContainer(containerName) {
478
+ await this.forceRemoveContainer(containerName, `Removed legacy mama container ${containerName} (pre-channel-isolation scheme)`, `Failed to remove legacy mama container ${containerName}`);
332
479
  }
333
480
  }
334
- /** @deprecated Use DockerContainerManager */
335
- export const DockerProvisioner = DockerContainerManager;
336
481
  //# sourceMappingURL=provisioner.js.map