@mono-agent/agent-app 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +15 -5
  2. package/dist/adapter-send-tools-main.js +2 -2
  3. package/dist/adapter-send-tools-main.js.map +1 -1
  4. package/dist/adapter-send-tools.d.ts +72 -6
  5. package/dist/adapter-send-tools.d.ts.map +1 -1
  6. package/dist/adapter-send-tools.js +413 -38
  7. package/dist/adapter-send-tools.js.map +1 -1
  8. package/dist/app-config.d.ts +48 -11
  9. package/dist/app-config.d.ts.map +1 -1
  10. package/dist/app-config.js +98 -24
  11. package/dist/app-config.js.map +1 -1
  12. package/dist/app.d.ts +10 -0
  13. package/dist/app.d.ts.map +1 -1
  14. package/dist/app.js +659 -48
  15. package/dist/app.js.map +1 -1
  16. package/dist/artifact-retention.d.ts +32 -0
  17. package/dist/artifact-retention.d.ts.map +1 -0
  18. package/dist/artifact-retention.js +111 -0
  19. package/dist/artifact-retention.js.map +1 -0
  20. package/dist/audit-runs.d.ts +12 -0
  21. package/dist/audit-runs.d.ts.map +1 -0
  22. package/dist/audit-runs.js +68 -0
  23. package/dist/audit-runs.js.map +1 -0
  24. package/dist/backfill.d.ts +3 -7
  25. package/dist/backfill.d.ts.map +1 -1
  26. package/dist/backfill.js +115 -11
  27. package/dist/backfill.js.map +1 -1
  28. package/dist/background.d.ts +8 -1
  29. package/dist/background.d.ts.map +1 -1
  30. package/dist/background.js +202 -4
  31. package/dist/background.js.map +1 -1
  32. package/dist/broadcast-recorder.d.ts +27 -0
  33. package/dist/broadcast-recorder.d.ts.map +1 -0
  34. package/dist/broadcast-recorder.js +82 -0
  35. package/dist/broadcast-recorder.js.map +1 -0
  36. package/dist/channel-config-view.d.ts +28 -0
  37. package/dist/channel-config-view.d.ts.map +1 -0
  38. package/dist/channel-config-view.js +79 -0
  39. package/dist/channel-config-view.js.map +1 -0
  40. package/dist/channel-gate.d.ts +22 -0
  41. package/dist/channel-gate.d.ts.map +1 -0
  42. package/dist/channel-gate.js +35 -0
  43. package/dist/channel-gate.js.map +1 -0
  44. package/dist/channel-plugins.d.ts +39 -0
  45. package/dist/channel-plugins.d.ts.map +1 -0
  46. package/dist/channel-plugins.js +372 -0
  47. package/dist/channel-plugins.js.map +1 -0
  48. package/dist/channels.d.ts +62 -73
  49. package/dist/channels.d.ts.map +1 -1
  50. package/dist/channels.js +833 -126
  51. package/dist/channels.js.map +1 -1
  52. package/dist/cli.d.ts +65 -3
  53. package/dist/cli.d.ts.map +1 -1
  54. package/dist/cli.js +755 -30
  55. package/dist/cli.js.map +1 -1
  56. package/dist/config-reference.d.ts +24 -0
  57. package/dist/config-reference.d.ts.map +1 -0
  58. package/dist/config-reference.js +580 -0
  59. package/dist/config-reference.js.map +1 -0
  60. package/dist/configured-agent.d.ts +104 -0
  61. package/dist/configured-agent.d.ts.map +1 -0
  62. package/dist/configured-agent.js +578 -0
  63. package/dist/configured-agent.js.map +1 -0
  64. package/dist/consumer-contract.d.ts +36 -0
  65. package/dist/consumer-contract.d.ts.map +1 -0
  66. package/dist/consumer-contract.js +290 -0
  67. package/dist/consumer-contract.js.map +1 -0
  68. package/dist/doctor.d.ts +9 -0
  69. package/dist/doctor.d.ts.map +1 -1
  70. package/dist/doctor.js +352 -55
  71. package/dist/doctor.js.map +1 -1
  72. package/dist/index.d.ts +9 -3
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +5 -2
  75. package/dist/index.js.map +1 -1
  76. package/dist/init.d.ts +23 -7
  77. package/dist/init.d.ts.map +1 -1
  78. package/dist/init.js +91 -26
  79. package/dist/init.js.map +1 -1
  80. package/dist/install-skill.d.ts +1 -1
  81. package/dist/install-skill.js +2 -2
  82. package/dist/install-skill.js.map +1 -1
  83. package/dist/interaction-bridge.d.ts +72 -0
  84. package/dist/interaction-bridge.d.ts.map +1 -0
  85. package/dist/interaction-bridge.js +354 -0
  86. package/dist/interaction-bridge.js.map +1 -0
  87. package/dist/memory-command.d.ts +10 -0
  88. package/dist/memory-command.d.ts.map +1 -0
  89. package/dist/memory-command.js +649 -0
  90. package/dist/memory-command.js.map +1 -0
  91. package/dist/memory-recall-main.js +1 -1
  92. package/dist/memory-recall-main.js.map +1 -1
  93. package/dist/memory-recall.d.ts +41 -5
  94. package/dist/memory-recall.d.ts.map +1 -1
  95. package/dist/memory-recall.js +75 -5
  96. package/dist/memory-recall.js.map +1 -1
  97. package/dist/memory-rituals.d.ts +8 -11
  98. package/dist/memory-rituals.d.ts.map +1 -1
  99. package/dist/memory-rituals.js +26 -25
  100. package/dist/memory-rituals.js.map +1 -1
  101. package/dist/metrics.d.ts +13 -0
  102. package/dist/metrics.d.ts.map +1 -0
  103. package/dist/metrics.js +105 -0
  104. package/dist/metrics.js.map +1 -0
  105. package/dist/notify-destinations.d.ts +23 -0
  106. package/dist/notify-destinations.d.ts.map +1 -0
  107. package/dist/notify-destinations.js +79 -0
  108. package/dist/notify-destinations.js.map +1 -0
  109. package/dist/posted-message-index.d.ts +61 -0
  110. package/dist/posted-message-index.d.ts.map +1 -0
  111. package/dist/posted-message-index.js +178 -0
  112. package/dist/posted-message-index.js.map +1 -0
  113. package/dist/proactive-notify.d.ts +32 -0
  114. package/dist/proactive-notify.d.ts.map +1 -0
  115. package/dist/proactive-notify.js +62 -0
  116. package/dist/proactive-notify.js.map +1 -0
  117. package/dist/recipes/base.d.ts +15 -0
  118. package/dist/recipes/base.d.ts.map +1 -0
  119. package/dist/recipes/base.js +51 -0
  120. package/dist/recipes/base.js.map +1 -0
  121. package/dist/recipes/catalog.d.ts +4 -0
  122. package/dist/recipes/catalog.d.ts.map +1 -0
  123. package/dist/recipes/catalog.js +525 -0
  124. package/dist/recipes/catalog.js.map +1 -0
  125. package/dist/recipes/index.d.ts +11 -0
  126. package/dist/recipes/index.d.ts.map +1 -0
  127. package/dist/recipes/index.js +14 -0
  128. package/dist/recipes/index.js.map +1 -0
  129. package/dist/recipes/types.d.ts +70 -0
  130. package/dist/recipes/types.d.ts.map +1 -0
  131. package/dist/recipes/types.js +15 -0
  132. package/dist/recipes/types.js.map +1 -0
  133. package/dist/request-model-override.d.ts +67 -0
  134. package/dist/request-model-override.d.ts.map +1 -0
  135. package/dist/request-model-override.js +96 -0
  136. package/dist/request-model-override.js.map +1 -0
  137. package/dist/runs-health.d.ts +22 -0
  138. package/dist/runs-health.d.ts.map +1 -0
  139. package/dist/runs-health.js +144 -0
  140. package/dist/runs-health.js.map +1 -0
  141. package/dist/seen-conversations.d.ts +22 -0
  142. package/dist/seen-conversations.d.ts.map +1 -0
  143. package/dist/seen-conversations.js +120 -0
  144. package/dist/seen-conversations.js.map +1 -0
  145. package/dist/setup.d.ts +29 -0
  146. package/dist/setup.d.ts.map +1 -0
  147. package/dist/setup.js +97 -0
  148. package/dist/setup.js.map +1 -0
  149. package/dist/trigger-overrides.d.ts +17 -0
  150. package/dist/trigger-overrides.d.ts.map +1 -0
  151. package/dist/trigger-overrides.js +30 -0
  152. package/dist/trigger-overrides.js.map +1 -0
  153. package/dist/tui-command.d.ts +56 -0
  154. package/dist/tui-command.d.ts.map +1 -0
  155. package/dist/tui-command.js +160 -0
  156. package/dist/tui-command.js.map +1 -0
  157. package/dist/ui.d.ts.map +1 -1
  158. package/dist/ui.js +4 -0
  159. package/dist/ui.js.map +1 -1
  160. package/dist/web-command.d.ts +45 -0
  161. package/dist/web-command.d.ts.map +1 -0
  162. package/dist/web-command.js +152 -0
  163. package/dist/web-command.js.map +1 -0
  164. package/package.json +19 -18
  165. package/schema/mono-agent.config.schema.json +1686 -0
  166. package/skills/mono-agent-composer/SKILL.md +34 -9
  167. package/skills/mono-agent-composer/agents/openai.yaml +4 -0
  168. package/skills/mono-agent-composer/references/config-blueprint.md +92 -49
  169. package/skills/mono-agent-composer/references/discovery-questions.md +15 -17
  170. package/skills/mono-agent-composer/references/feature-coverage.md +22 -17
  171. package/skills/mono-agent-composer/references/package-map.md +19 -21
  172. package/skills/mono-agent-composer/references/playbooks.md +267 -0
  173. package/skills/mono-agent-composer/references/validation.md +9 -1
package/dist/app.js CHANGED
@@ -1,11 +1,20 @@
1
1
  import { resolve } from "node:path";
2
- import { createConfiguredAgentResponder, createConfiguredAgentRuntime, createConfiguredMemory, } from "@mono-agent/agent-host";
3
- import { registerTraceSource } from "@mono-agent/observability";
4
- import { isAppCoreConfigError, loadAppCoreConfig, resolveAppArtifactDir, resolveAppObservabilityExporters, resolveAppTraceHeartbeatMs, resolveAppTraceRegistryDir, resolveAppTraceSourceId, resolveAppTraceSourceLabel, resolveAppTraceStaleAfterMs, } from "./app-config.js";
5
- import { defaultChannelDrivers } from "./channels.js";
6
- import { adapterSendToolNames, createAdapterSendToolsRuntimeExtension, resolveAdapterSendToolsSettings, } from "./adapter-send-tools.js";
2
+ import { createLiveEventBus } from "@mono-agent/agent-contracts";
3
+ import { pruneTraceSources, reconcileStaleRunArtifacts, registerTraceSource } from "@mono-agent/observability";
4
+ import { modelReferenceKey } from "@mono-agent/runtime-adapter";
5
+ import { describeSandboxEffectiveState, resolveSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
6
+ import { isAppCoreConfigError, loadAppCoreConfig, resolveAppArtifactDir, resolveAppObservabilityExporters, resolveAppTraceGlobalDiscovery, resolveAppTraceHeartbeatMs, resolveAppTraceRegistryDir, resolveAppTraceSourceId, resolveAppTraceSourceLabel, resolveAppTraceStaleAfterMs, resolveGlobalTraceRegistryDir, resolveTraceTmpdirRoot, shouldMirrorTraceSourceGlobally, } from "./app-config.js";
7
+ import { createConfiguredAgentResponder, createConfiguredAgentRuntime, createConfiguredMemory, } from "./configured-agent.js";
8
+ import { resolveChannelDrivers } from "./channels.js";
9
+ import { routeProactiveNotification } from "./proactive-notify.js";
10
+ import { adapterSendToolNames, createAdapterSendToolsRuntimeExtension, isAdapterSendToolAllowed, resolveAdapterSendToolsSettings, } from "./adapter-send-tools.js";
11
+ import { loadInteractionSettings, startInteractionBridge } from "./interaction-bridge.js";
7
12
  import { startMemoryRituals } from "./memory-rituals.js";
13
+ import { startArtifactRetentionScheduler } from "./artifact-retention.js";
8
14
  import { createMemoryRecallRuntimeExtension, resolveMemoryRecallSettings, } from "./memory-recall.js";
15
+ import { createRequestModelOverrideRuntimeExtension } from "./request-model-override.js";
16
+ import { resolveNotifyDestinations } from "./notify-destinations.js";
17
+ import { resolvePostedMessageIndexPath } from "./posted-message-index.js";
9
18
  /**
10
19
  * Starts a config-first mono-agent host in `cwd`: traceability first, then every
11
20
  * configured channel in parallel. Channels with incomplete config report
@@ -16,7 +25,7 @@ export async function startMonoAgentApp(options = {}) {
16
25
  const cwd = resolve(options.cwd ?? process.cwd());
17
26
  const configPath = resolve(cwd, options.configPath ?? "mono-agent.config.json");
18
27
  const env = options.env ?? process.env;
19
- const drivers = options.drivers ?? defaultChannelDrivers();
28
+ const drivers = options.drivers ?? await resolveChannelDrivers({ env, cwd, configPath });
20
29
  const controller = new MonoAgentAppController({
21
30
  cwd,
22
31
  configPath,
@@ -24,8 +33,10 @@ export async function startMonoAgentApp(options = {}) {
24
33
  drivers,
25
34
  ...(options.logger === undefined ? {} : { logger: options.logger }),
26
35
  ...(options.runtime === undefined ? {} : { runtime: options.runtime }),
36
+ ...(options.sandboxEngine === undefined ? {} : { sandboxEngine: options.sandboxEngine }),
27
37
  ...(options.traceDefaults === undefined ? {} : { traceDefaults: options.traceDefaults }),
28
38
  });
39
+ await controller.refreshSandboxStatus("startup");
29
40
  await controller.startTraceability("startup");
30
41
  await controller.startExporters("startup");
31
42
  await Promise.all(drivers.map((driver) => controller?.startChannelIfConfigured(driver.id, "startup")));
@@ -33,6 +44,16 @@ export async function startMonoAgentApp(options = {}) {
33
44
  await controller.refreshTraceSource("startup-complete");
34
45
  return controller;
35
46
  }
47
+ const DEFAULT_SANDBOX_STATUS = sandboxStatusFromState({
48
+ configured: false,
49
+ configuredMode: undefined,
50
+ effective: "off",
51
+ engine: undefined,
52
+ engineAvailable: undefined,
53
+ fallback: undefined,
54
+ fallbackActive: false,
55
+ unsafeAllowHostProcess: false,
56
+ });
36
57
  class MonoAgentAppController {
37
58
  configPath;
38
59
  cwd;
@@ -41,11 +62,15 @@ class MonoAgentAppController {
41
62
  driversById;
42
63
  logger;
43
64
  runtime;
65
+ sandboxEngine;
44
66
  traceDefaults;
45
67
  activeRuntimes = [];
46
68
  statuses = new Map();
47
69
  running = new Map();
48
70
  startsInFlight = new Map();
71
+ /** Captured at construction (~process start): the cutoff for reclaiming orphaned "running" runs. */
72
+ processStartMs = Date.now();
73
+ staleRunsReconciled = false;
49
74
  traceabilityStatusValue = {
50
75
  kind: "disabled",
51
76
  reason: "Traceability has not started yet.",
@@ -54,16 +79,38 @@ class MonoAgentAppController {
54
79
  kind: "disabled",
55
80
  reason: "No observability exporter configured.",
56
81
  };
82
+ sandboxStatusValue = DEFAULT_SANDBOX_STATUS;
83
+ selectedSkillsValue;
84
+ sessionMetadataValue;
57
85
  /** The exporter the responder threads into agent-host (first configured exporter). */
58
86
  resolvedExporter;
59
87
  traceSource;
88
+ traceRefreshTail = Promise.resolve();
89
+ // Best-effort mirror of `traceSource` into the machine-wide global registry,
90
+ // present only when `shouldMirrorTraceSourceGlobally` gates it on (see
91
+ // `startTraceability`). Kept in lockstep with `traceSource` on every
92
+ // refresh/stop so both manifests describe the same instance identically.
93
+ globalTraceSource;
94
+ artifactRetentionScheduler;
95
+ artifactRetentionGeneration = 0;
60
96
  memoryRituals;
61
97
  // One shared memory store across all channel responders + the ritual scheduler, so there is a single
62
98
  // memory.db handle (not one per channel plus one for rituals). Rebuilt on config reload, closed on stop.
63
99
  sharedMemory = undefined;
64
100
  sharedMemoryBuilt = false;
101
+ sharedMemoryBuild;
65
102
  configApplyTail = Promise.resolve();
66
103
  stopped = false;
104
+ // Interaction bridge (ask_user + tool progress): lazily started once, shared
105
+ // by every channel; the exported env keys are tracked for cleanup on stop.
106
+ interactionBridge;
107
+ interactionBridgeStart;
108
+ interactionBridgeEnvKeys = [];
109
+ // Shared in-process run-event bus: every run's recorder publishes to it (via the
110
+ // broadcast recorder threaded as `runEventSink`), and the `live` channel relays
111
+ // it over SSE. One instance for the app's lifetime — cheap, bounded ring buffer,
112
+ // and it must exist before any responder is built (like the interaction bridge).
113
+ liveEventBus = createLiveEventBus();
67
114
  constructor(input) {
68
115
  this.cwd = input.cwd;
69
116
  this.configPath = input.configPath;
@@ -72,6 +119,7 @@ class MonoAgentAppController {
72
119
  this.driversById = new Map(input.drivers.map((driver) => [driver.id, driver]));
73
120
  this.logger = input.logger;
74
121
  this.runtime = input.runtime;
122
+ this.sandboxEngine = input.sandboxEngine;
75
123
  this.traceDefaults = input.traceDefaults;
76
124
  for (const driver of input.drivers) {
77
125
  this.statuses.set(driver.id, {
@@ -86,6 +134,12 @@ class MonoAgentAppController {
86
134
  get exporterStatus() {
87
135
  return this.exporterStatusValue;
88
136
  }
137
+ get sandboxStatus() {
138
+ return this.sandboxStatusValue;
139
+ }
140
+ get selectedSkills() {
141
+ return this.selectedSkillsValue;
142
+ }
89
143
  channelStatus(id) {
90
144
  const status = this.statuses.get(id);
91
145
  if (status === undefined) {
@@ -109,8 +163,10 @@ class MonoAgentAppController {
109
163
  await this.stopChannel(driver.id, `${reason}:reload`);
110
164
  }
111
165
  this.stopMemoryRituals();
166
+ this.stopArtifactRetentionScheduler();
112
167
  await this.resetSharedMemory();
113
168
  await this.stopTraceSource(`${reason}:reload`);
169
+ await this.refreshSandboxStatus(reason);
114
170
  await this.startTraceability(reason);
115
171
  await this.startExporters(reason);
116
172
  await Promise.all(this.drivers.map((driver) => this.startChannelIfConfigured(driver.id, reason)));
@@ -147,16 +203,20 @@ class MonoAgentAppController {
147
203
  if (this.stopped) {
148
204
  return this.traceabilityStatusValue;
149
205
  }
206
+ let artifactDirForRetention;
150
207
  try {
151
208
  const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
152
- const [registryDir, artifactDir, sourceId, label, heartbeatMs] = await Promise.all([
209
+ await this.refreshSelectedSkillsSnapshot(reason);
210
+ const [registryDir, artifactDir, sourceId, label, heartbeatMs, globalDiscovery] = await Promise.all([
153
211
  resolveAppTraceRegistryDir(input),
154
212
  resolveAppArtifactDir(input),
155
213
  resolveAppTraceSourceId(input, this.traceDefaults),
156
214
  resolveAppTraceSourceLabel(input, this.traceDefaults),
157
215
  resolveAppTraceHeartbeatMs(input),
216
+ resolveAppTraceGlobalDiscovery(input),
158
217
  ]);
159
- this.traceSource = await registerTraceSource({
218
+ artifactDirForRetention = artifactDir;
219
+ const registerOptions = {
160
220
  registryDir,
161
221
  sourceId,
162
222
  label,
@@ -166,17 +226,102 @@ class MonoAgentAppController {
166
226
  configPath: this.configPath,
167
227
  metadata: this.traceMetadata(reason),
168
228
  heartbeatMs,
169
- });
229
+ };
230
+ this.traceSource = await registerTraceSource(registerOptions);
170
231
  this.traceabilityStatusValue = { kind: "running", sourceId, registryDir, artifactDir };
171
232
  this.logger?.info?.("Traceability source registered.", { reason, sourceId, registryDir, artifactDir });
233
+ void pruneTraceSources({ registryDir });
234
+ // Best-effort global mirror: makes this agent discoverable by `mono-agent
235
+ // tui` run anywhere on the machine, even when its own registryDir is a
236
+ // config-local override (e.g. `mono-agent init`'s scaffold). A mirror
237
+ // failure must never affect the primary registration above.
238
+ const globalRegistryDir = resolveGlobalTraceRegistryDir(this.env);
239
+ const tmpdirRoot = resolveTraceTmpdirRoot(this.env);
240
+ if (shouldMirrorTraceSourceGlobally({ registryDir, globalRegistryDir, globalDiscovery, tmpdirRoot })) {
241
+ try {
242
+ this.globalTraceSource = await registerTraceSource({ ...registerOptions, registryDir: globalRegistryDir });
243
+ void pruneTraceSources({ registryDir: globalRegistryDir });
244
+ }
245
+ catch (error) {
246
+ this.globalTraceSource = undefined;
247
+ this.logger?.warn?.("Global trace-source mirror registration failed.", { reason: reasonOf(error) });
248
+ }
249
+ }
250
+ else {
251
+ this.globalTraceSource = undefined;
252
+ }
172
253
  }
173
254
  catch (error) {
174
255
  const failure = reasonOf(error);
175
256
  this.traceabilityStatusValue = { kind: "failed", reason: failure };
176
257
  this.logger?.error?.("Traceability source registration failed.", { reason: failure });
177
258
  }
259
+ finally {
260
+ if (artifactDirForRetention !== undefined) {
261
+ // Fire-and-forget: orphan reclamation and retention are best-effort cleanup
262
+ // and must not gate startup readiness. Keep them independent from trace
263
+ // source registration; a broken registry should not disable artifact GC.
264
+ this.restartArtifactRetentionScheduler(artifactDirForRetention, reason);
265
+ }
266
+ }
178
267
  return this.traceabilityStatusValue;
179
268
  }
269
+ async refreshSandboxStatus(reason) {
270
+ try {
271
+ const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
272
+ const coreConfig = await loadAppCoreConfig(input);
273
+ const state = await resolveSandboxEffectiveState({
274
+ ...(coreConfig.sandbox === undefined ? {} : { policy: coreConfig.sandbox }),
275
+ ...(this.sandboxEngine === undefined ? {} : { engine: this.sandboxEngine }),
276
+ });
277
+ const status = sandboxStatusFromState(state);
278
+ this.sandboxStatusValue = status;
279
+ if (status.warning !== undefined) {
280
+ this.logger?.warn?.(status.warning, { reason, detail: status.detail });
281
+ }
282
+ return status;
283
+ }
284
+ catch (error) {
285
+ const resolutionError = reasonOf(error);
286
+ const status = {
287
+ ...DEFAULT_SANDBOX_STATUS,
288
+ detail: `Sandbox status unavailable until agent config loads: ${resolutionError}`,
289
+ resolutionError,
290
+ };
291
+ this.sandboxStatusValue = status;
292
+ this.logger?.info?.("Sandbox status unavailable until agent config loads.", { reason, detail: resolutionError });
293
+ return status;
294
+ }
295
+ }
296
+ /**
297
+ * One-shot at startup: a process that crashed mid-run leaves its summary stuck at "running"
298
+ * forever (a ghost run in `status`/observability). Reclaim those — any "running" summary that
299
+ * began before THIS process started — by rewriting them to "interrupted". Gated so a config
300
+ * reload (which re-runs startTraceability) does not repeat the scan. Best-effort: never fatal.
301
+ */
302
+ async reconcileStaleRunsOnce(artifactDir) {
303
+ if (this.staleRunsReconciled) {
304
+ return;
305
+ }
306
+ this.staleRunsReconciled = true;
307
+ try {
308
+ const { reconciled, warnings } = await reconcileStaleRunArtifacts(artifactDir, {
309
+ startedBeforeMs: this.processStartMs,
310
+ });
311
+ for (const warning of warnings) {
312
+ this.logger?.warn?.(`Stale-run reconciliation: ${warning}`);
313
+ }
314
+ if (reconciled.length > 0) {
315
+ this.logger?.info?.('Reclaimed orphaned runs left as "running" by a prior process.', {
316
+ count: reconciled.length,
317
+ runIds: reconciled.slice(0, 20),
318
+ });
319
+ }
320
+ }
321
+ catch (error) {
322
+ this.logger?.warn?.("Stale-run reconciliation failed.", { reason: reasonOf(error) });
323
+ }
324
+ }
180
325
  /**
181
326
  * Resolve the configured observability exporter(s) and publish the export
182
327
  * status. No reachability probe runs here — Phoenix may start after the agent,
@@ -235,18 +380,84 @@ class MonoAgentAppController {
235
380
  void this.refreshTraceSource("exporter-warning").catch(() => undefined);
236
381
  }
237
382
  async refreshTraceSource(reason) {
238
- if (this.traceSource === undefined) {
239
- return;
240
- }
241
- try {
242
- await this.traceSource.update({
243
- transports: this.activeTransports(),
244
- metadata: this.traceMetadata(reason),
383
+ const run = async () => {
384
+ if (this.traceSource === undefined) {
385
+ return;
386
+ }
387
+ const patch = { transports: this.activeTransports(), metadata: this.traceMetadata(reason) };
388
+ try {
389
+ await this.traceSource.update(patch);
390
+ }
391
+ catch (error) {
392
+ this.logger?.warn?.("Traceability source update failed.", { reason: reasonOf(error) });
393
+ }
394
+ if (this.globalTraceSource !== undefined) {
395
+ try {
396
+ await this.globalTraceSource.update(patch);
397
+ }
398
+ catch (error) {
399
+ this.logger?.warn?.("Global trace-source mirror update failed.", { reason: reasonOf(error) });
400
+ }
401
+ }
402
+ };
403
+ const next = this.traceRefreshTail.then(run, run);
404
+ this.traceRefreshTail = next.then(() => undefined, () => undefined);
405
+ await next;
406
+ }
407
+ /**
408
+ * Start the interaction bridge once, when the ask_user tool is allowed by the
409
+ * tool policy or the operator configured the `interaction` block. Exports the
410
+ * bridge env into the app env AND process env so settings resolution and
411
+ * spawned stdio tool children (which inherit process.env) can reach it.
412
+ */
413
+ ensureInteractionBridge(coreConfig) {
414
+ this.interactionBridgeStart ??= (async () => {
415
+ const askUserAllowed = isAdapterSendToolAllowed("ask_user", {
416
+ allowedTools: coreConfig.tools.allowedTools,
417
+ disallowedTools: coreConfig.tools.disallowedTools,
245
418
  });
419
+ const settings = await loadInteractionSettings({ env: this.env, configPath: this.configPath });
420
+ if (!askUserAllowed && !settings.configured) {
421
+ return undefined;
422
+ }
423
+ try {
424
+ const bridge = await startInteractionBridge({
425
+ host: settings.host,
426
+ port: settings.port,
427
+ askTimeoutMs: settings.askTimeoutMs,
428
+ ...(this.logger === undefined ? {} : { logger: this.logger }),
429
+ });
430
+ const bridgeEnv = bridge.env();
431
+ this.interactionBridgeEnvKeys = Object.keys(bridgeEnv);
432
+ Object.assign(this.env, bridgeEnv);
433
+ if (this.env !== process.env) {
434
+ Object.assign(process.env, bridgeEnv);
435
+ }
436
+ this.interactionBridge = bridge;
437
+ this.logger?.info?.("Interaction bridge started.", { url: bridge.url });
438
+ return bridge;
439
+ }
440
+ catch (error) {
441
+ this.logger?.warn?.("Interaction bridge failed to start; ask_user and tool progress are unavailable.", {
442
+ reason: reasonOf(error),
443
+ });
444
+ return undefined;
445
+ }
446
+ })();
447
+ return this.interactionBridgeStart;
448
+ }
449
+ async stopInteractionBridge() {
450
+ const bridge = this.interactionBridge;
451
+ this.interactionBridge = undefined;
452
+ this.interactionBridgeStart = undefined;
453
+ for (const key of this.interactionBridgeEnvKeys) {
454
+ delete this.env[key];
455
+ if (this.env !== process.env) {
456
+ delete process.env[key];
457
+ }
246
458
  }
247
- catch (error) {
248
- this.logger?.warn?.("Traceability source update failed.", { reason: reasonOf(error) });
249
- }
459
+ this.interactionBridgeEnvKeys = [];
460
+ await bridge?.stop().catch(() => undefined);
250
461
  }
251
462
  async stop() {
252
463
  if (this.stopped) {
@@ -256,7 +467,9 @@ class MonoAgentAppController {
256
467
  for (const driver of this.drivers) {
257
468
  await this.stopChannel(driver.id, "stop");
258
469
  }
470
+ await this.stopInteractionBridge();
259
471
  this.stopMemoryRituals();
472
+ this.stopArtifactRetentionScheduler();
260
473
  await this.resetSharedMemory();
261
474
  await this.stopTraceSource("stop");
262
475
  for (const runtime of this.activeRuntimes.splice(0)) {
@@ -271,26 +484,30 @@ class MonoAgentAppController {
271
484
  try {
272
485
  const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
273
486
  coreConfig = await loadAppCoreConfig(input);
487
+ this.rememberSelectedSkills(coreConfig);
274
488
  }
275
489
  catch {
276
- // Config not ready yet — rituals will start on the next applyConfigChange.
490
+ // Config not ready yet — consolidation will start on the next applyConfigChange.
277
491
  return;
278
492
  }
279
493
  if (coreConfig.memory?.mode !== "bujo") {
280
494
  return;
281
495
  }
282
- const runtime = this.runtime ?? createConfiguredAgentRuntime(coreConfig);
496
+ const runtime = this.runtime ?? createConfiguredAgentRuntime({
497
+ config: coreConfig,
498
+ ...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
499
+ });
283
500
  if (!this.activeRuntimes.includes(runtime)) {
284
501
  this.activeRuntimes.push(runtime);
285
502
  }
286
- const store = this.memoryStore(coreConfig, runtime);
287
- // Duck-type: only bujo-tier BujoMemoryStore has reflect/migrate.
503
+ const store = await this.memoryStore(coreConfig);
504
+ // Duck-type: only bujo-tier BujoMemoryStore has consolidate().
288
505
  // Cast through unknown to bypass the MemoryStore contract's type mismatch.
289
506
  const storeAsAny = store;
290
507
  if (store === undefined ||
291
- typeof storeAsAny["reflect"] !== "function" ||
292
- typeof storeAsAny["migrate"] !== "function" ||
508
+ typeof storeAsAny["consolidate"] !== "function" ||
293
509
  typeof storeAsAny["tier"] !== "function") {
510
+ this.logger?.info?.("Memory consolidation scheduler skipped — store does not support consolidate().", { reason });
294
511
  return;
295
512
  }
296
513
  const bujoStore = store;
@@ -299,13 +516,12 @@ class MonoAgentAppController {
299
516
  // scheduler started in that case — log an accurate skip instead.
300
517
  const tier = bujoStore.tier();
301
518
  if (tier !== "bujo") {
302
- this.logger?.info?.("Memory ritual scheduler skipped — store tier is not bujo (reflect/migrate need a chat LLM).", { reason, tier });
519
+ this.logger?.info?.("Memory consolidation scheduler skipped — configured bujo mode resolved to the journal tier because memory.llm is missing.", { reason, tier });
303
520
  return;
304
521
  }
305
522
  this.memoryRituals = startMemoryRituals({
306
523
  store: bujoStore,
307
- ...(coreConfig.memory.reflection !== undefined && { reflection: coreConfig.memory.reflection }),
308
- ...(coreConfig.memory.migration !== undefined && { migration: coreConfig.memory.migration }),
524
+ ...(coreConfig.memory.consolidation !== undefined && { consolidation: coreConfig.memory.consolidation }),
309
525
  ...(this.logger !== undefined && {
310
526
  logger: {
311
527
  info: (m) => this.logger?.info?.(m),
@@ -313,7 +529,7 @@ class MonoAgentAppController {
313
529
  },
314
530
  }),
315
531
  });
316
- this.logger?.info?.("Memory ritual scheduler started.", { reason, mode: "bujo" });
532
+ this.logger?.info?.("Memory consolidation scheduler started.", { reason, mode: "bujo" });
317
533
  }
318
534
  stopMemoryRituals() {
319
535
  const rituals = this.memoryRituals;
@@ -322,7 +538,94 @@ class MonoAgentAppController {
322
538
  }
323
539
  this.memoryRituals = undefined;
324
540
  rituals.stop();
325
- this.logger?.info?.("Memory ritual scheduler stopped.");
541
+ this.logger?.info?.("Memory consolidation scheduler stopped.");
542
+ }
543
+ restartArtifactRetentionScheduler(artifactDir, reason) {
544
+ this.stopArtifactRetentionScheduler();
545
+ const generation = ++this.artifactRetentionGeneration;
546
+ void (async () => {
547
+ let coreConfig;
548
+ try {
549
+ const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
550
+ coreConfig = await loadAppCoreConfig(input);
551
+ this.rememberSelectedSkills(coreConfig);
552
+ }
553
+ catch (error) {
554
+ this.logger?.warn?.("Artifact retention scheduler skipped until core config loads.", { reason: reasonOf(error) });
555
+ void this.reconcileStaleRunsOnce(artifactDir);
556
+ return;
557
+ }
558
+ if (this.stopped || generation !== this.artifactRetentionGeneration) {
559
+ return;
560
+ }
561
+ this.artifactRetentionScheduler = startArtifactRetentionScheduler({
562
+ artifactDir,
563
+ retention: coreConfig.artifacts.retention,
564
+ memoryRetention: coreConfig.artifacts.memoryRetention,
565
+ ...(this.logger === undefined ? {} : { logger: this.logger }),
566
+ beforeFirstRun: () => this.reconcileStaleRunsOnce(artifactDir),
567
+ });
568
+ this.logger?.info?.("Artifact retention scheduler started.", {
569
+ reason,
570
+ artifactDir,
571
+ agent: {
572
+ maxAgeDays: coreConfig.artifacts.retention.maxAgeDays,
573
+ maxCount: coreConfig.artifacts.retention.maxCount,
574
+ dryRun: coreConfig.artifacts.retention.dryRun,
575
+ },
576
+ memory: {
577
+ maxAgeDays: coreConfig.artifacts.memoryRetention.maxAgeDays,
578
+ maxCount: coreConfig.artifacts.memoryRetention.maxCount,
579
+ dryRun: coreConfig.artifacts.memoryRetention.dryRun,
580
+ },
581
+ });
582
+ })();
583
+ }
584
+ stopArtifactRetentionScheduler() {
585
+ this.artifactRetentionGeneration += 1;
586
+ const scheduler = this.artifactRetentionScheduler;
587
+ if (scheduler === undefined) {
588
+ return;
589
+ }
590
+ this.artifactRetentionScheduler = undefined;
591
+ scheduler.stop();
592
+ this.logger?.info?.("Artifact retention scheduler stopped.");
593
+ }
594
+ /**
595
+ * Deliver a native cron/webhook notification to whichever running channel owns
596
+ * the destination conversationId. With `verbatim`, the channel posts `text`
597
+ * unchanged (no model call) and records it to history; otherwise it runs `text`
598
+ * as a turn. Best-effort: an unavailable/unsupported destination is warned and
599
+ * skipped, never thrown back to the cron/webhook trigger.
600
+ */
601
+ async notifyDestination(conversationId, text, options) {
602
+ const result = await routeProactiveNotification({
603
+ conversationId,
604
+ text,
605
+ running: this.running,
606
+ ...(options?.verbatim === undefined ? {} : { verbatim: options.verbatim }),
607
+ ...(this.logger === undefined ? {} : { logger: this.logger }),
608
+ });
609
+ // Make the delivery outcome inspectable (the failure cases already warn inside
610
+ // the router / channel hooks; log the success path too so a notify is auditable).
611
+ if (result.delivered) {
612
+ this.logger?.info?.("Proactive notification delivered.", { conversationId });
613
+ }
614
+ return result;
615
+ }
616
+ /**
617
+ * Candidate destinations for native cron/webhook notification delivery, used to
618
+ * infer a target when a job/endpoint sets no explicit `notifyConversationId`.
619
+ */
620
+ async listNotifyDestinations() {
621
+ const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
622
+ const artifactDir = await resolveAppArtifactDir(input);
623
+ return await resolveNotifyDestinations({
624
+ input,
625
+ artifactDir,
626
+ isRunning: (id) => this.running.has(id),
627
+ ...(this.logger === undefined ? {} : { logger: this.logger }),
628
+ });
326
629
  }
327
630
  async startChannel(driver, reason) {
328
631
  const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
@@ -344,9 +647,16 @@ class MonoAgentAppController {
344
647
  if (waitingReason !== undefined) {
345
648
  return this.setStatus(driver.id, { kind: "waiting_for_config", reason: waitingReason });
346
649
  }
650
+ // Structural issues (e.g. an invalid per-trigger model override) fail
651
+ // `validate` but only WARN here: the run-time override path ignores the
652
+ // bad value and falls back, so starting is still the safe choice.
653
+ for (const issue of driver.configIssues?.(config) ?? []) {
654
+ this.logger?.warn?.("Channel config issue (run `mono-agent validate`).", { channel: driver.id, issue });
655
+ }
347
656
  let coreConfig;
348
657
  try {
349
658
  coreConfig = await loadAppCoreConfig(input);
659
+ this.rememberSelectedSkills(coreConfig);
350
660
  }
351
661
  catch (error) {
352
662
  if (isAppCoreConfigError(error)) {
@@ -355,6 +665,13 @@ class MonoAgentAppController {
355
665
  }
356
666
  throw error;
357
667
  }
668
+ // Resolve the posted-message index path once so the Slack driver can link
669
+ // posted messages to their producing conversation (in-thread reply continuity).
670
+ const postedMessageIndexPath = resolvePostedMessageIndexPath(await resolveAppArtifactDir(input));
671
+ // The bridge must exist BEFORE the responder is built (ask_user settings
672
+ // resolution reads the exported bridge env) and before driver.start (sink
673
+ // registration + pending-ask interception).
674
+ const interactionBridge = await this.ensureInteractionBridge(coreConfig);
358
675
  try {
359
676
  const responder = await this.buildResponder(coreConfig);
360
677
  // The AgentResponder contract has no dispose(), but the configured responder
@@ -371,6 +688,11 @@ class MonoAgentAppController {
371
688
  coreConfig,
372
689
  responder,
373
690
  cwd: this.cwd,
691
+ notifyDestination: (conversationId, text, options) => this.notifyDestination(conversationId, text, options),
692
+ listNotifyDestinations: () => this.listNotifyDestinations(),
693
+ postedMessageIndexPath,
694
+ ...(interactionBridge === undefined ? {} : { interaction: interactionBridge }),
695
+ liveEventBus: this.liveEventBus,
374
696
  ...(this.logger === undefined ? {} : { logger: this.logger }),
375
697
  onFailure: (failureReason) => {
376
698
  this.running.delete(driver.id);
@@ -387,6 +709,32 @@ class MonoAgentAppController {
387
709
  });
388
710
  });
389
711
  },
712
+ // A self-recovering transport (e.g. telegram poll crash on a network blip).
713
+ // Unlike onFailure, this must NOT delete the running entry or dispose the
714
+ // responder: the channel owns its own restart, so the harness stays alive
715
+ // for the restarted transport to deliver into, and a later stop/reload still
716
+ // finds the entry and disposes it exactly once.
717
+ onDegraded: (reason) => {
718
+ if (this.statuses.get(driver.id)?.kind === "degraded") {
719
+ return;
720
+ }
721
+ this.setStatus(driver.id, { kind: "degraded", reason });
722
+ this.logger?.warn?.(`${driver.label} channel degraded; transport is recovering.`, { reason });
723
+ },
724
+ // The transport's self-recovery succeeded. Flip back to running, reusing the
725
+ // preserved entry's summary. Guard on the entry: a recovery that races a
726
+ // stop/reload must not resurrect a torn-down channel's status.
727
+ onRecovered: () => {
728
+ const entry = this.running.get(driver.id);
729
+ if (entry === undefined) {
730
+ return;
731
+ }
732
+ if (this.statuses.get(driver.id)?.kind !== "degraded") {
733
+ return;
734
+ }
735
+ this.setStatus(driver.id, { kind: "running", summary: entry.summary });
736
+ this.logger?.info?.(`${driver.label} channel recovered.`, {});
737
+ },
390
738
  });
391
739
  this.running.set(driver.id, {
392
740
  ...runningChannel,
@@ -431,14 +779,33 @@ class MonoAgentAppController {
431
779
  if (!this.activeRuntimes.includes(runtime)) {
432
780
  this.activeRuntimes.push(runtime);
433
781
  }
434
- const memory = this.memoryStore(coreConfig, runtime);
782
+ const memory = await this.memoryStore(coreConfig);
435
783
  const memoryRecall = this.memoryRecallRuntimeOptions(coreConfig);
784
+ const supermemoryMcp = this.supermemoryMcpRuntimeOptions(coreConfig);
436
785
  const adapterSendTools = await this.adapterSendToolsRuntimeOptions(coreConfig);
437
- const runtimeOptionsForRequest = composeRuntimeOptionExtensions([memoryRecall, adapterSendTools]);
786
+ // Always active: a no-op for interactive turns (which carry no cron/webhook
787
+ // metadata), it applies the per-trigger model/effort override otherwise.
788
+ const requestModelOverride = this.requestModelOverrideRuntimeOptions(coreConfig);
789
+ const runtimeOptionsForRequest = composeRuntimeOptionExtensions([
790
+ memoryRecall,
791
+ supermemoryMcp,
792
+ adapterSendTools,
793
+ requestModelOverride,
794
+ ]);
795
+ // The override factory is only needed when fallbacks are configured: the
796
+ // fallback router freezes the model chain, so an override must run on a runtime
797
+ // whose chain has it as primary. With no fallbacks the shared (plain) runtime
798
+ // honors the per-run model directly, so building a separate runtime would be
799
+ // redundant. Omit the factory there and the harness uses the shared runtime.
800
+ const runtimeForModel = (coreConfig.runtime.fallbackModels?.length ?? 0) > 0
801
+ ? this.buildRuntimeForModel(coreConfig)
802
+ : undefined;
438
803
  const observabilityContext = await this.observabilityContext();
439
- const responder = createConfiguredAgentResponder({
804
+ const responder = await createConfiguredAgentResponder({
440
805
  config: coreConfig,
441
806
  runtime,
807
+ ...(runtimeForModel === undefined ? {} : { runtimeForModel }),
808
+ ...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
442
809
  ...(memory !== undefined && { memory }),
443
810
  ...(runtimeOptionsForRequest === undefined ? {} : { runtimeOptionsForRequest }),
444
811
  // Thread run-identifying context onto exported spans and surface per-run
@@ -446,9 +813,54 @@ class MonoAgentAppController {
446
813
  // when config.observability.exporters is non-empty).
447
814
  observabilityContext,
448
815
  exporterWarn: (warning) => this.recordExporterWarning(warning),
816
+ onSessionEvent: (event) => this.recordSessionEvent(event, coreConfig),
817
+ // Publish every run's start/event/finish to the shared bus so the `live`
818
+ // channel can relay it. Best-effort + additive (see broadcast recorder).
819
+ runEventSink: this.liveEventBus,
449
820
  });
450
821
  return responder;
451
822
  }
823
+ /**
824
+ * Per-request extension that applies a cron/webhook/tui per-trigger model +
825
+ * effort override (validated, warn-and-ignore on bad input). Threads the
826
+ * configured local providers so a LOCAL-model override recomputes its endpoint
827
+ * block for the OVERRIDE model (see request-model-override doc). Composed
828
+ * alongside the memory/adapter extensions.
829
+ */
830
+ requestModelOverrideRuntimeOptions(coreConfig) {
831
+ const extension = createRequestModelOverrideRuntimeExtension({
832
+ ...(this.logger === undefined ? {} : { logger: this.logger }),
833
+ ...(coreConfig.providers?.local === undefined ? {} : { localProviders: coreConfig.providers.local }),
834
+ });
835
+ return async (input) => extension({ request: input.request });
836
+ }
837
+ /**
838
+ * Memoized factory for runtimes bound to a per-request override model. Reuses
839
+ * createConfiguredAgentRuntime so the override becomes the fallback-chain
840
+ * primary with the configured backups after it (override + keep fallbacks).
841
+ * Built runtimes register in `activeRuntimes`, which is disposed on `stop()`
842
+ * (config reload rebuilds the responder but does not drain prior runtimes —
843
+ * same lifetime as the base runtime built in `buildResponder`).
844
+ */
845
+ buildRuntimeForModel(coreConfig) {
846
+ const cache = new Map();
847
+ return (model, executionMode) => {
848
+ const key = `${modelReferenceKey(model)}|${executionMode ?? ""}`;
849
+ const cached = cache.get(key);
850
+ if (cached !== undefined) {
851
+ return cached;
852
+ }
853
+ const runtime = createConfiguredAgentRuntime({
854
+ config: coreConfig,
855
+ model,
856
+ ...(executionMode === undefined ? {} : { executionMode }),
857
+ ...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
858
+ });
859
+ cache.set(key, runtime);
860
+ this.activeRuntimes.push(runtime);
861
+ return runtime;
862
+ };
863
+ }
452
864
  /**
453
865
  * Run-identifying context threaded onto exported spans (Phoenix shows the same
454
866
  * source/run identifiers as the local trace-source registry, so local artifact
@@ -469,10 +881,37 @@ class MonoAgentAppController {
469
881
  return undefined;
470
882
  }
471
883
  this.logger?.info?.("Read-only memory_recall tool enabled.", {
472
- provider: settings.embeddings?.provider ?? "fts-only",
884
+ provider: "supermemory" in settings ? "supermemory" : settings.embeddings?.provider ?? "fts-only",
473
885
  });
474
886
  return createMemoryRecallRuntimeExtension(settings, this.cwd);
475
887
  }
888
+ /**
889
+ * Optional CLOUD-ONLY escape hatch: when `memory.supermemory.exposeMcpServer` is on, ALSO inject
890
+ * Supermemory's hosted MCP server alongside the in-app `memory_recall` tool. The hosted MCP cannot
891
+ * point at a self-hosted instance, so self-hosters rely on the in-app recall tool; this just adds
892
+ * the cloud server's richer tools for cloud deployments. Requires an apiKey (skipped + warned if
893
+ * absent).
894
+ */
895
+ supermemoryMcpRuntimeOptions(coreConfig) {
896
+ const memory = coreConfig.memory;
897
+ if (memory?.backend !== "supermemory" || memory.supermemory?.exposeMcpServer !== true) {
898
+ return undefined;
899
+ }
900
+ const apiKey = memory.supermemory.apiKey;
901
+ if (apiKey === undefined) {
902
+ this.logger?.warn?.("memory.supermemory.exposeMcpServer is on but no apiKey is set; the hosted Supermemory MCP server (cloud-only) was not injected.");
903
+ return undefined;
904
+ }
905
+ this.logger?.info?.("Supermemory hosted MCP server injected (cloud-only).");
906
+ const entry = {
907
+ supermemory: {
908
+ type: "http",
909
+ url: "https://mcp.supermemory.ai/mcp",
910
+ headers: { Authorization: `Bearer ${apiKey}` },
911
+ },
912
+ };
913
+ return async () => ({ runtimeOptions: { mcpServers: entry }, cleanup: async () => { } });
914
+ }
476
915
  async adapterSendToolsRuntimeOptions(coreConfig) {
477
916
  const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
478
917
  const settings = await resolveAdapterSendToolsSettings(input, {
@@ -485,25 +924,60 @@ class MonoAgentAppController {
485
924
  }
486
925
  const toolNames = adapterSendToolNames(settings);
487
926
  this.logger?.info?.("Adapter send tools enabled.", { tools: toolNames });
488
- return createAdapterSendToolsRuntimeExtension(this.configPath, this.cwd, toolNames);
927
+ // Forward the posted-message index path so `slack_send_message` links each post
928
+ // back to the producing conversation (so a later in-thread reply resumes it).
929
+ const indexPath = resolvePostedMessageIndexPath(await resolveAppArtifactDir(input));
930
+ return createAdapterSendToolsRuntimeExtension(this.configPath, this.cwd, toolNames, indexPath);
489
931
  }
490
932
  /** Build the configured memory store once and share it across responders + the ritual scheduler. */
491
- memoryStore(coreConfig, runtime) {
492
- if (!this.sharedMemoryBuilt) {
933
+ async memoryStore(coreConfig) {
934
+ if (this.sharedMemoryBuilt) {
935
+ return this.sharedMemory;
936
+ }
937
+ if (this.sharedMemoryBuild !== undefined) {
938
+ return await this.sharedMemoryBuild;
939
+ }
940
+ this.sharedMemoryBuild = (async () => {
493
941
  const appLogger = this.logger;
494
942
  const logger = appLogger?.warn !== undefined
495
943
  ? { warn: (message) => { appLogger.warn?.(message); } }
496
944
  : undefined;
497
- this.sharedMemory = createConfiguredMemory(coreConfig, { ...(logger === undefined ? {} : { logger }), runtime });
945
+ // Thread the per-app observability context so the bujo memory LLM records
946
+ // capture and consolidation runs through the same JSONL + Phoenix pipeline
947
+ // as channel runs (gated by `memory.llm.trace`, default on). The context is
948
+ // per-app (not per-request), so caching it into the shared store is correct.
949
+ //
950
+ // The channel runtime is intentionally NOT passed: the memory LLM must run
951
+ // on `config.memory.llm.model`, but the channel runtime carries the channel
952
+ // fallback chain whose primary is `config.runtime.model` and the fallback
953
+ // router overrides each run's per-call model. createConfiguredMemory builds
954
+ // the memory LLM its own fallback-free runtime when no `memoryRuntime` is set.
955
+ const observabilityContext = await this.observabilityContext();
956
+ const observability = {
957
+ observabilityContext,
958
+ exporterWarn: (warning) => this.recordExporterWarning(warning),
959
+ runEventSink: this.liveEventBus,
960
+ };
961
+ this.sharedMemory = await createConfiguredMemory(coreConfig, {
962
+ ...(logger === undefined ? {} : { logger }),
963
+ observability,
964
+ });
498
965
  this.sharedMemoryBuilt = true;
966
+ return this.sharedMemory;
967
+ })();
968
+ try {
969
+ return await this.sharedMemoryBuild;
970
+ }
971
+ finally {
972
+ this.sharedMemoryBuild = undefined;
499
973
  }
500
- return this.sharedMemory;
501
974
  }
502
975
  /** Close + clear the shared memory store (on config reload or stop) so the next build is fresh. */
503
976
  async resetSharedMemory() {
504
977
  const mem = this.sharedMemory;
505
978
  this.sharedMemory = undefined;
506
979
  this.sharedMemoryBuilt = false;
980
+ this.sharedMemoryBuild = undefined;
507
981
  if (mem?.flush !== undefined) {
508
982
  await Promise.resolve(mem.flush()).catch(() => undefined);
509
983
  }
@@ -522,7 +996,8 @@ class MonoAgentAppController {
522
996
  }
523
997
  applyResult() {
524
998
  const transports = this.activeTransports();
525
- const statuses = [...this.statuses.values()];
999
+ const statusEntries = [...this.statuses.entries()];
1000
+ const statuses = statusEntries.map(([, status]) => status);
526
1001
  const failedChannel = statuses.find((status) => status.kind === "failed");
527
1002
  const failure = failedChannel?.kind === "failed"
528
1003
  ? failedChannel.reason
@@ -536,8 +1011,10 @@ class MonoAgentAppController {
536
1011
  transports,
537
1012
  };
538
1013
  }
539
- const hasRunningChannel = statuses.some((status) => status.kind === "running");
540
- if (!hasRunningChannel && statuses.some((status) => status.kind === "waiting_for_config")) {
1014
+ // A degraded channel is still serving (transport self-recovering, harness alive),
1015
+ // so it counts as running for the "is anything live?" check.
1016
+ const hasServingChannel = statusEntries.some(([id, status]) => id !== "live" && (status.kind === "running" || status.kind === "degraded"));
1017
+ if (!hasServingChannel && statuses.some((status) => status.kind === "waiting_for_config")) {
541
1018
  return {
542
1019
  kind: "waiting_for_config",
543
1020
  message: "Saved config, but no agent channel is running yet.",
@@ -552,16 +1029,19 @@ class MonoAgentAppController {
552
1029
  }
553
1030
  async stopTraceSource(reason) {
554
1031
  const traceSource = this.traceSource;
555
- if (traceSource === undefined) {
1032
+ const globalTraceSource = this.globalTraceSource;
1033
+ if (traceSource === undefined && globalTraceSource === undefined) {
556
1034
  return;
557
1035
  }
558
1036
  this.traceSource = undefined;
559
- await traceSource.stop({
560
- metadata: this.traceMetadata(reason),
561
- transports: this.activeTransports(),
562
- }).catch((error) => {
1037
+ this.globalTraceSource = undefined;
1038
+ const patch = { metadata: this.traceMetadata(reason), transports: this.activeTransports() };
1039
+ await traceSource?.stop(patch).catch((error) => {
563
1040
  this.logger?.warn?.("Traceability source stop update failed.", { reason: reasonOf(error) });
564
1041
  });
1042
+ await globalTraceSource?.stop(patch).catch((error) => {
1043
+ this.logger?.warn?.("Global trace-source mirror stop update failed.", { reason: reasonOf(error) });
1044
+ });
565
1045
  if (!this.stopped) {
566
1046
  this.traceabilityStatusValue = { kind: "disabled", reason: "Traceability source stopped while applying config." };
567
1047
  }
@@ -569,12 +1049,57 @@ class MonoAgentAppController {
569
1049
  activeTransports() {
570
1050
  const transports = [];
571
1051
  for (const driver of this.drivers) {
572
- if (this.statuses.get(driver.id)?.kind === "running") {
1052
+ const kind = this.statuses.get(driver.id)?.kind;
1053
+ // A degraded channel is still an active transport (self-recovering, serving).
1054
+ if (kind === "running" || kind === "degraded") {
573
1055
  transports.push(driver.id);
574
1056
  }
575
1057
  }
576
1058
  return transports;
577
1059
  }
1060
+ async refreshSelectedSkillsSnapshot(reason) {
1061
+ try {
1062
+ const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
1063
+ this.rememberSelectedSkills(await loadAppCoreConfig(input));
1064
+ }
1065
+ catch (error) {
1066
+ this.selectedSkillsValue = undefined;
1067
+ this.logger?.info?.("Selected skills unavailable until agent config loads.", { reason, detail: reasonOf(error) });
1068
+ }
1069
+ }
1070
+ rememberSelectedSkills(coreConfig) {
1071
+ this.selectedSkillsValue = [...coreConfig.context.selectedSkills];
1072
+ }
1073
+ recordSessionEvent(event, coreConfig) {
1074
+ const now = new Date();
1075
+ const nextRolloverAt = coreConfig.runtime.session.rollover === "daily"
1076
+ ? nextDailyRolloverAt(now, coreConfig.runtime.session.rolloverTimezone)
1077
+ : undefined;
1078
+ const snapshot = event.snapshot ?? [];
1079
+ const current = snapshot.find((entry) => entry.conversationId === event.conversationId);
1080
+ const providerSessionId = current?.providerSessionId ?? event.providerSessionId;
1081
+ this.sessionMetadataValue = {
1082
+ currentBucketId: event.conversationId,
1083
+ state: current === undefined ? "cold" : "warm",
1084
+ event: event.kind,
1085
+ updatedAt: now.toISOString(),
1086
+ ...(snapshot.length === 0 ? {} : { snapshot }),
1087
+ ...(providerSessionId === undefined ? {} : { providerSessionId }),
1088
+ ...(current?.createdAt === undefined ? {} : { createdAt: current.createdAt }),
1089
+ ...(current?.lastActivityAt === undefined ? {} : { lastActivityAt: current.lastActivityAt }),
1090
+ ...(current?.busy === undefined ? {} : { busy: current.busy }),
1091
+ ...(event.reason === undefined ? {} : { reason: event.reason }),
1092
+ ...(nextRolloverAt === undefined ? {} : { nextRolloverAt }),
1093
+ };
1094
+ if (event.kind === "evicted") {
1095
+ this.logger?.info?.("Provider session evicted.", {
1096
+ conversationId: event.conversationId,
1097
+ providerSessionId: event.providerSessionId,
1098
+ reason: event.reason,
1099
+ });
1100
+ }
1101
+ void this.refreshTraceSource(`session-${event.kind}`).catch(() => undefined);
1102
+ }
578
1103
  traceMetadata(reason) {
579
1104
  const channels = {};
580
1105
  for (const driver of this.drivers) {
@@ -606,10 +1131,96 @@ class MonoAgentAppController {
606
1131
  },
607
1132
  }
608
1133
  : {}),
1134
+ sandbox: {
1135
+ configured: this.sandboxStatusValue.configured,
1136
+ configuredMode: this.sandboxStatusValue.configuredMode,
1137
+ effective: this.sandboxStatusValue.effective,
1138
+ engine: this.sandboxStatusValue.engine,
1139
+ engineAvailable: this.sandboxStatusValue.engineAvailable,
1140
+ fallback: this.sandboxStatusValue.fallback,
1141
+ fallbackActive: this.sandboxStatusValue.fallbackActive,
1142
+ unsafeAllowHostProcess: this.sandboxStatusValue.unsafeAllowHostProcess,
1143
+ detail: this.sandboxStatusValue.detail,
1144
+ ...(this.sandboxStatusValue.warning === undefined ? {} : { warning: this.sandboxStatusValue.warning }),
1145
+ ...(this.sandboxStatusValue.resolutionError === undefined
1146
+ ? {}
1147
+ : { resolutionError: this.sandboxStatusValue.resolutionError }),
1148
+ },
1149
+ ...(this.selectedSkillsValue === undefined
1150
+ ? {}
1151
+ : {
1152
+ context: {
1153
+ selectedSkills: [...this.selectedSkillsValue],
1154
+ },
1155
+ }),
1156
+ ...(this.sessionMetadataValue === undefined ? {} : { session: this.sessionMetadataValue }),
609
1157
  channels,
610
1158
  };
611
1159
  }
612
1160
  }
1161
+ function nextDailyRolloverAt(now, timezone) {
1162
+ if (timezone === undefined || timezone.trim().length === 0) {
1163
+ const next = new Date(now);
1164
+ next.setHours(24, 0, 0, 0);
1165
+ return next.toISOString();
1166
+ }
1167
+ try {
1168
+ const parts = datePartsInTimeZone(now, timezone);
1169
+ return new Date(zonedDateTimeToUtcMs({
1170
+ year: parts.year,
1171
+ month: parts.month,
1172
+ day: parts.day + 1,
1173
+ hour: 0,
1174
+ minute: 0,
1175
+ second: 0,
1176
+ }, timezone)).toISOString();
1177
+ }
1178
+ catch {
1179
+ const next = new Date(now);
1180
+ next.setHours(24, 0, 0, 0);
1181
+ return next.toISOString();
1182
+ }
1183
+ }
1184
+ function zonedDateTimeToUtcMs(parts, timezone) {
1185
+ const targetAsUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
1186
+ let utcMs = targetAsUtc - timeZoneOffsetMs(new Date(targetAsUtc), timezone);
1187
+ utcMs = targetAsUtc - timeZoneOffsetMs(new Date(utcMs), timezone);
1188
+ return utcMs;
1189
+ }
1190
+ function timeZoneOffsetMs(date, timezone) {
1191
+ const parts = datePartsInTimeZone(date, timezone);
1192
+ const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
1193
+ return asUtc - date.getTime();
1194
+ }
1195
+ function datePartsInTimeZone(date, timezone) {
1196
+ const formatter = new Intl.DateTimeFormat("en-CA", {
1197
+ timeZone: timezone,
1198
+ year: "numeric",
1199
+ month: "2-digit",
1200
+ day: "2-digit",
1201
+ hour: "2-digit",
1202
+ minute: "2-digit",
1203
+ second: "2-digit",
1204
+ hourCycle: "h23",
1205
+ });
1206
+ const entries = new Map(formatter.formatToParts(date).map((part) => [part.type, part.value]));
1207
+ return {
1208
+ year: Number(entries.get("year")),
1209
+ month: Number(entries.get("month")),
1210
+ day: Number(entries.get("day")),
1211
+ hour: Number(entries.get("hour")),
1212
+ minute: Number(entries.get("minute")),
1213
+ second: Number(entries.get("second")),
1214
+ };
1215
+ }
1216
+ function sandboxStatusFromState(state) {
1217
+ const warning = sandboxEffectiveStateWarning(state);
1218
+ return {
1219
+ ...state,
1220
+ detail: describeSandboxEffectiveState(state),
1221
+ ...(warning === undefined ? {} : { warning }),
1222
+ };
1223
+ }
613
1224
  function reasonOf(error) {
614
1225
  return error instanceof Error ? error.message : String(error);
615
1226
  }