@ouro.bot/cli 0.1.0-alpha.810 → 0.1.0-alpha.812

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 (40) hide show
  1. package/changelog.json +18 -0
  2. package/deploy/unraid/README.txt +3 -6
  3. package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
  4. package/deploy/unraid/sanctuary-deployment-target.mjs +1 -0
  5. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +8 -7
  6. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  7. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +1 -1
  8. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  9. package/deploy/unraid/sanctuary.xml +1 -1
  10. package/dist/heart/config.js +32 -2
  11. package/dist/heart/core.js +167 -115
  12. package/dist/heart/daemon/daemon.js +19 -9
  13. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +88 -50
  14. package/dist/heart/daemon/sanctuary-acceptance-harness.js +53 -15
  15. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +6 -26
  16. package/dist/heart/frontend-approval-runtime.js +87 -13
  17. package/dist/heart/identity.js +17 -5
  18. package/dist/heart/session-events.js +8 -1
  19. package/dist/heart/tool-approval.js +64 -31
  20. package/dist/mind/pending.js +4 -4
  21. package/dist/repertoire/mcp-manager.js +365 -381
  22. package/dist/repertoire/mcp-tools.js +66 -25
  23. package/dist/repertoire/plugin-mcp.js +3 -3
  24. package/dist/repertoire/shell-sessions.js +8 -7
  25. package/dist/repertoire/tool-arguments.js +30 -8
  26. package/dist/repertoire/tools-session.js +30 -5
  27. package/dist/repertoire/tools-shell.js +30 -14
  28. package/dist/repertoire/tools-voice.js +6 -6
  29. package/dist/repertoire/tools.js +231 -205
  30. package/dist/senses/bluebubbles/index.js +4 -2
  31. package/dist/senses/cli.js +6 -5
  32. package/dist/senses/private-runtime.js +9 -3
  33. package/dist/senses/shared-turn.js +12 -9
  34. package/dist/senses/teams.js +15 -12
  35. package/dist/senses/telegram-approval-runtime.js +129 -20
  36. package/dist/senses/telegram-client.js +19 -2
  37. package/dist/senses/telegram.js +54 -15
  38. package/dist/senses/voice/twilio-phone.js +123 -173
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
@@ -37,6 +37,7 @@ exports.settleFrontendApproval = settleFrontendApproval;
37
37
  exports.createFrontendApprovalRuntime = createFrontendApprovalRuntime;
38
38
  const node_crypto_1 = require("node:crypto");
39
39
  const path = __importStar(require("node:path"));
40
+ const friends_1 = require("@ouro.bot/friends");
40
41
  const approval_files_1 = require("./approval-files");
41
42
  const approval_store_1 = require("./approval-store");
42
43
  const tool_approval_1 = require("./tool-approval");
@@ -52,12 +53,10 @@ const PERMISSION_OPTIONS = [
52
53
  { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
53
54
  { optionId: "reject-once", name: "Reject", kind: "reject_once" },
54
55
  ];
55
- /* v8 ignore start -- lazy default adapter avoids widening every daemon test mock; settlement behavior is covered through the injected dependency @preserve */
56
- async function defaultReleaseRuntimeMcpServers() {
56
+ async function defaultReleaseRuntimeMcpServers(owner) {
57
57
  const manager = await Promise.resolve().then(() => __importStar(require("../repertoire/mcp-manager")));
58
- await manager.releaseRuntimeMcpServers();
58
+ await manager.releaseRuntimeMcpServers(owner);
59
59
  }
60
- /* v8 ignore stop */
61
60
  const DEFAULT_SETTLEMENT_DEPENDENCIES = {
62
61
  withTurnExecutionLease: turn_execution_lease_1.withTurnExecutionLease,
63
62
  setAgentName: identity_1.setAgentName,
@@ -66,7 +65,10 @@ const DEFAULT_SETTLEMENT_DEPENDENCIES = {
66
65
  executeApprovalDecision: tool_approval_1.executeApprovalDecision,
67
66
  resolveToolDefinition: tools_1.resolveToolDefinition,
68
67
  approvalPolicyForInvocation: tools_1.approvalPolicyForInvocation,
69
- execTool: tools_1.execTool,
68
+ executeTool: tools_1.executeTool,
69
+ preflightToolCall: tools_1.preflightToolCall,
70
+ selectToolsForChannel: tools_1.selectToolsForChannel,
71
+ getProviderRuntime: core_1.getProviderRuntime,
70
72
  getSharedMcpManager: mcp_manager_1.getSharedMcpManager,
71
73
  releaseRuntimeMcpServers: defaultReleaseRuntimeMcpServers,
72
74
  loadSession: context_1.loadSession,
@@ -86,15 +88,18 @@ async function settleFrontendApproval(input, dependencies = DEFAULT_SETTLEMENT_D
86
88
  const record = input.approvalStore.read(input.suspension.approvalId);
87
89
  if (!record)
88
90
  throw new Error(`frontend approval record is unavailable: ${input.suspension.approvalId}`);
91
+ const owner = Object.freeze({ agentName: input.request.agent, agentRoot: input.agentRoot });
92
+ const toolsDisabled = input.request.disableTools === true;
93
+ const runtimeServers = input.request.runtimeMcpServers;
89
94
  return dependencies.withTurnExecutionLease(async () => {
90
- dependencies.setAgentName(input.request.agent);
95
+ dependencies.setAgentName(owner.agentName);
91
96
  try {
92
- const mcpManager = await dependencies.getSharedMcpManager(input.request.runtimeMcpServers ? { runtimeServers: input.request.runtimeMcpServers } : undefined) ?? undefined;
93
97
  return await dependencies.withSessionTurnLease(record.sessionPath, async (lease) => {
94
98
  const currentRevision = dependencies.readSessionTransaction(record.sessionPath, lease).revision;
95
99
  const continuationSignal = input.optionId === "cancelled" ? AbortSignal.abort() : input.signal;
96
100
  const liveToolContext = input.approvalRequest.liveToolContext ?? {
97
101
  signin: async () => undefined,
102
+ agentName: owner.agentName,
98
103
  agentRoot: input.agentRoot,
99
104
  currentSession: {
100
105
  friendId: input.request.friendId,
@@ -104,6 +109,47 @@ async function settleFrontendApproval(input, dependencies = DEFAULT_SETTLEMENT_D
104
109
  },
105
110
  currentUserMessage: input.request.message,
106
111
  };
112
+ const currentOptions = async () => {
113
+ try {
114
+ if ((liveToolContext.agentName !== undefined && liveToolContext.agentName !== owner.agentName)
115
+ || (liveToolContext.agentRoot !== undefined && liveToolContext.agentRoot !== owner.agentRoot)) {
116
+ throw new Error("approval context owner coordinates changed");
117
+ }
118
+ let context = { ...liveToolContext, ...owner };
119
+ const original = liveToolContext.relationshipAuthorization;
120
+ if (original) {
121
+ if (liveToolContext.agentName !== owner.agentName || liveToolContext.agentRoot !== owner.agentRoot) {
122
+ throw new Error("approval context owner coordinates changed");
123
+ }
124
+ const current = await original.resolveCurrent?.();
125
+ if (!current?.actor || current.actor.friendId !== input.request.friendId
126
+ || current.actor.trustLevel !== original.actor?.trustLevel || current.profileId !== original.profileId
127
+ || original.authorizedContextScopes.some((scope) => !current.authorizedContextScopes.includes(scope))) {
128
+ throw new Error("current approval relationship authority is unavailable");
129
+ }
130
+ context = { ...context, relationshipAuthorization: current };
131
+ }
132
+ const providerRuntime = await dependencies.getProviderRuntime((0, friends_1.channelToFacing)(input.request.channel), owner);
133
+ const mcpManager = toolsDisabled ? undefined : await dependencies.getSharedMcpManager({ ...owner, runtimeServers }) ?? undefined;
134
+ const selectCurrentTools = () => toolsDisabled
135
+ ? Object.freeze({ ordinary: Object.freeze([]), engine: Object.freeze([]) })
136
+ : dependencies.selectToolsForChannel((0, friends_1.getChannelCapabilities)(input.request.channel), context.context?.friend.toolPreferences, context.context, providerRuntime.capabilities, mcpManager, providerRuntime.model, context);
137
+ return {
138
+ providerRuntimeOverride: providerRuntime, mcpManager,
139
+ ...(toolsDisabled ? { tools: [], hardDisableTools: true } : {}),
140
+ toolContext: { ...context, toolSelection: selectCurrentTools(), selectCurrentTools },
141
+ };
142
+ }
143
+ catch (error) {
144
+ (0, runtime_1.emitNervesEvent)({
145
+ level: "warn", component: "heart", event: "heart.frontend_approval_authority_unavailable",
146
+ message: "current approval authority could not be reconstructed; execution remains disabled",
147
+ meta: { approvalId: record.approvalId, category: error instanceof Error ? error.name : "unknown" },
148
+ });
149
+ return null;
150
+ }
151
+ };
152
+ let definition;
107
153
  const terminal = await dependencies.executeApprovalDecision({
108
154
  approvalStore: input.approvalStore,
109
155
  checkpointStore: input.checkpointStore,
@@ -121,11 +167,36 @@ async function settleFrontendApproval(input, dependencies = DEFAULT_SETTLEMENT_D
121
167
  },
122
168
  ownerId: `frontend-decision-${dependencies.randomUUID()}`,
123
169
  currentSessionRevision: currentRevision,
124
- resolveTool: dependencies.resolveToolDefinition,
125
- resolveApprovalPolicy: (name, args) => dependencies.approvalPolicyForInvocation(name, args, liveToolContext),
170
+ resolveTool: async (name) => {
171
+ const current = await currentOptions();
172
+ definition = current ? dependencies.resolveToolDefinition(name, current.toolContext.toolSelection) : undefined;
173
+ return definition;
174
+ },
175
+ resolveApprovalPolicy: async (name, args) => {
176
+ const current = await currentOptions();
177
+ return current ? dependencies.approvalPolicyForInvocation(name, args, current.toolContext) : { kind: "not_required" };
178
+ },
126
179
  liveGuard: async () => ({ ok: true }),
127
180
  liveRisk: async () => ({ ok: true }),
128
- execute: (name, args) => dependencies.execTool(name, args, liveToolContext),
181
+ preflight: async (context) => {
182
+ const current = await currentOptions();
183
+ if (!current)
184
+ return { ok: false, reason: "current tool authority is unavailable" };
185
+ const result = await dependencies.preflightToolCall(context.record.toolName, context.arguments, {
186
+ ...current.toolContext,
187
+ toolSelection: Object.freeze({ ordinary: Object.freeze([context.definition]), engine: Object.freeze([]) }),
188
+ });
189
+ return result.kind === "ready" ? { ok: true } : { ok: false, reason: result.text };
190
+ },
191
+ execute: async (name, args) => {
192
+ const current = await currentOptions();
193
+ if (!current || !definition)
194
+ return { kind: "rejected_before_handler", text: "current approved tool authority is unavailable" };
195
+ return dependencies.executeTool(name, args, {
196
+ ...current.toolContext,
197
+ toolSelection: Object.freeze({ ordinary: Object.freeze([definition]), engine: Object.freeze([]) }),
198
+ });
199
+ },
129
200
  });
130
201
  input.tokenStore.remove(record.approvalId);
131
202
  const existing = dependencies.loadSession(record.sessionPath);
@@ -204,10 +275,13 @@ async function settleFrontendApproval(input, dependencies = DEFAULT_SETTLEMENT_D
204
275
  },
205
276
  runAgent: dependencies.runAgent,
206
277
  runAgentOptions: {
207
- mcpManager,
208
278
  toolContext: liveToolContext,
209
279
  approvalCoordinator: nestedCoordinator,
210
280
  },
281
+ revalidate: async () => {
282
+ const current = await currentOptions();
283
+ return current ? { ...current, approvalCoordinator: nestedCoordinator } : null;
284
+ },
211
285
  persist,
212
286
  deliver: async (text) => {
213
287
  response = text;
@@ -242,8 +316,8 @@ async function settleFrontendApproval(input, dependencies = DEFAULT_SETTLEMENT_D
242
316
  });
243
317
  }
244
318
  finally {
245
- if (input.request.runtimeMcpServers) {
246
- await dependencies.releaseRuntimeMcpServers();
319
+ if (runtimeServers && !toolsDisabled) {
320
+ await dependencies.releaseRuntimeMcpServers(owner);
247
321
  }
248
322
  }
249
323
  });
@@ -386,15 +386,27 @@ function validateFacingConfig(parsed, facingName, configFile) {
386
386
  }
387
387
  /**
388
388
  * Load and parse `<agentRoot>/agent.json`.
389
- * Reads the file fresh on each call unless an override is set.
389
+ * Reads the explicit owner's file fresh, or preserves the legacy ambient override when no owner is supplied.
390
390
  * If the config is v1, auto-migrates to v2 via migrateAgentConfigV1ToV2 and re-reads.
391
391
  * Throws descriptive error if file is missing or contains invalid JSON.
392
392
  */
393
- function loadAgentConfig() {
394
- if (_agentConfigOverride) {
393
+ function loadAgentConfig(owner) {
394
+ if (owner !== undefined && (typeof owner !== "object" || owner === null
395
+ || typeof owner.agentName !== "string" || owner.agentName.length === 0
396
+ || typeof owner.agentRoot !== "string" || !path.isAbsolute(owner.agentRoot))) {
397
+ (0, runtime_1.emitNervesEvent)({
398
+ level: "error",
399
+ event: "config_identity.error",
400
+ component: "config/identity",
401
+ message: "invalid agent configuration owner",
402
+ meta: { reason: "explicit owner requires an agent name and absolute root" },
403
+ });
404
+ throw new Error("invalid agent configuration owner");
405
+ }
406
+ if (owner === undefined && _agentConfigOverride) {
395
407
  return _agentConfigOverride;
396
408
  }
397
- const agentRoot = getAgentRoot();
409
+ const agentRoot = owner === undefined ? getAgentRoot() : owner.agentRoot;
398
410
  const configFile = path.join(agentRoot, "agent.json");
399
411
  let parsed = readAndParseAgentJson(configFile);
400
412
  // Inline migration: v1 -> v2
@@ -473,7 +485,7 @@ function loadAgentConfig() {
473
485
  enabled,
474
486
  humanFacing,
475
487
  agentFacing,
476
- habitPaidTurnsPerDay: (0, autonomy_budget_1.normalizeHabitPaidTurnsPerDay)(getAgentName(), parsed.habitPaidTurnsPerDay, configFile),
488
+ habitPaidTurnsPerDay: (0, autonomy_budget_1.normalizeHabitPaidTurnsPerDay)(owner === undefined ? getAgentName() : owner.agentName, parsed.habitPaidTurnsPerDay, configFile),
477
489
  senses: normalizeSenses(parsed.senses, configFile),
478
490
  phrases: parsed.phrases,
479
491
  };
@@ -127,11 +127,18 @@ function materializeApprovalTerminal(input) {
127
127
  }
128
128
  const advanced = input.currentSessionRevision !== input.checkpoint.suspendedSessionRevision;
129
129
  if (advanced) {
130
+ const notice = input.record.state === "succeeded"
131
+ ? "the approved action completed, but the session changed; no model continuation was run"
132
+ : input.record.state === "failed"
133
+ ? "the approved action failed, but the session changed; no model continuation was run"
134
+ : input.record.state === "attempted_indeterminate"
135
+ ? `${APPROVAL_TERMINAL_TEXT.attempted_indeterminate}; the session changed`
136
+ : "the session changed before this approval could be applied; the protected action was not executed";
130
137
  return {
131
138
  messages,
132
139
  materialized: false,
133
140
  resumeProvider: false,
134
- directNotice: "the session changed before this approval could be applied; the protected action was not executed",
141
+ directNotice: notice,
135
142
  revision: messagesRevision(messages),
136
143
  };
137
144
  }
@@ -5,6 +5,7 @@ exports.coordinateApprovalDecision = coordinateApprovalDecision;
5
5
  exports.digestApprovalSuspensionCheckpointPayload = digestApprovalSuspensionCheckpointPayload;
6
6
  exports.commitApprovalProposal = commitApprovalProposal;
7
7
  exports.recoverApprovalProposals = recoverApprovalProposals;
8
+ exports.digestApprovalToolDefinition = digestApprovalToolDefinition;
8
9
  exports.executeApprovalDecision = executeApprovalDecision;
9
10
  exports.recoverClaimedApproval = recoverClaimedApproval;
10
11
  exports.recoverAttemptedApproval = recoverAttemptedApproval;
@@ -238,47 +239,65 @@ function terminalizeClaimed(store, claimed, state, reason) {
238
239
  reason,
239
240
  });
240
241
  }
241
- async function executeApprovalDecision(options) {
242
- const decided = options.approvalStore.decide({ ...options.decision, ownerId: options.ownerId });
243
- if (decided.state !== "claimed")
244
- return decided;
245
- await options.hooks?.afterClaim?.();
246
- if (options.currentSessionRevision !== decided.suspendedSessionRevision) {
247
- return terminalizeClaimed(options.approvalStore, decided, "session_head_changed", "suspended session revision changed");
248
- }
249
- const checkpoint = options.checkpointStore.read(decided.approvalId);
250
- if (!checkpoint || !checkpointMatches(decided, checkpoint) || !frozenCallMatches(decided, checkpoint)) {
251
- return terminalizeClaimed(options.approvalStore, decided, "drifted", "checkpoint evidence drift");
252
- }
253
- const definition = options.resolveTool(decided.toolName);
242
+ function digestApprovalToolDefinition(definition, schemaDigest, policyId) {
243
+ const binding = definition.mcpBinding;
244
+ return (0, tool_arguments_1.digestJson)({
245
+ name: definition.tool.function.name, schemaDigest, policyId, advertisedTool: JSON.parse(JSON.stringify(definition.tool)),
246
+ ...(binding ? { mcp: {
247
+ agentName: binding.agentName, agentRoot: binding.agentRoot,
248
+ server: binding.server, rawName: binding.rawName, surfacedName: binding.surfacedName,
249
+ source: binding.source, pluginId: binding.pluginId ?? null,
250
+ configDigest: binding.configDigest,
251
+ } } : {}),
252
+ });
253
+ }
254
+ async function resolveApprovalTool(options, decided) {
255
+ const definition = await options.resolveTool(decided.toolName);
254
256
  if (!definition || definition.tool.function.name !== decided.toolName) {
255
- return terminalizeClaimed(options.approvalStore, decided, "drifted", "tool identity drift");
257
+ return { ok: false, reason: "tool identity drift" };
256
258
  }
257
259
  const schema = definition.tool.function.parameters;
258
260
  if (!schema || typeof schema !== "object") {
259
- return terminalizeClaimed(options.approvalStore, decided, "drifted", "advertised schema missing");
261
+ return { ok: false, reason: "advertised schema missing" };
260
262
  }
261
263
  const validated = (0, tool_arguments_1.validateAdvertisedToolArguments)(JSON.stringify(decided.arguments), schema);
262
264
  if (!validated.ok || validated.value.argumentDigest !== decided.argumentDigest
263
265
  || validated.value.schemaDigest !== decided.schemaDigest) {
264
- return terminalizeClaimed(options.approvalStore, decided, "drifted", "tool arguments or schema drift");
266
+ return { ok: false, reason: "tool arguments or schema drift" };
265
267
  }
266
268
  const policy = await options.resolveApprovalPolicy?.(decided.toolName, validated.value.arguments)
267
269
  ?? definition.approvalPolicy?.(validated.value.arguments)
268
270
  ?? { kind: "not_required" };
269
271
  if (policy.kind !== "required") {
270
- return terminalizeClaimed(options.approvalStore, decided, "drifted", "approval policy no longer requires approval");
272
+ return { ok: false, reason: "approval policy no longer requires approval" };
271
273
  }
272
- const toolDigest = (0, tool_arguments_1.digestJson)({ name: decided.toolName, schemaDigest: validated.value.schemaDigest, policyId: policy.policyId });
274
+ const toolDigest = digestApprovalToolDefinition(definition, validated.value.schemaDigest, policy.policyId);
273
275
  const policyDigest = (0, tool_arguments_1.digestJson)({ policyId: policy.policyId, actionClass: policy.actionClass, classification: "required" });
274
276
  if (policy.policyId !== decided.policyId || toolDigest !== decided.toolDigest || policyDigest !== decided.policyDigest) {
275
- return terminalizeClaimed(options.approvalStore, decided, "drifted", "approval policy or tool digest drift");
277
+ return { ok: false, reason: "approval policy or tool digest drift" };
276
278
  }
279
+ return { ok: true, definition, arguments: validated.value.arguments };
280
+ }
281
+ async function executeApprovalDecision(options) {
282
+ const decided = options.approvalStore.decide({ ...options.decision, ownerId: options.ownerId });
283
+ if (decided.state !== "claimed")
284
+ return decided;
285
+ await options.hooks?.afterClaim?.();
286
+ if (options.currentSessionRevision !== decided.suspendedSessionRevision) {
287
+ return terminalizeClaimed(options.approvalStore, decided, "session_head_changed", "suspended session revision changed");
288
+ }
289
+ const checkpoint = options.checkpointStore.read(decided.approvalId);
290
+ if (!checkpoint || !checkpointMatches(decided, checkpoint) || !frozenCallMatches(decided, checkpoint)) {
291
+ return terminalizeClaimed(options.approvalStore, decided, "drifted", "checkpoint evidence drift");
292
+ }
293
+ const current = await resolveApprovalTool(options, decided);
294
+ if (!current.ok)
295
+ return terminalizeClaimed(options.approvalStore, decided, "drifted", current.reason);
277
296
  const context = {
278
297
  record: structuredClone(decided),
279
298
  checkpoint: structuredClone(checkpoint),
280
- definition,
281
- arguments: structuredClone(validated.value.arguments),
299
+ definition: current.definition,
300
+ arguments: structuredClone(current.arguments),
282
301
  };
283
302
  const guard = await options.liveGuard(context);
284
303
  if (!guard.ok)
@@ -286,6 +305,14 @@ async function executeApprovalDecision(options) {
286
305
  const risk = await options.liveRisk(context);
287
306
  if (!risk.ok)
288
307
  return terminalizeClaimed(options.approvalStore, decided, "drifted", risk.reason);
308
+ const fresh = await resolveApprovalTool(options, decided);
309
+ if (!fresh.ok)
310
+ return terminalizeClaimed(options.approvalStore, decided, "drifted", fresh.reason);
311
+ const preflight = options.preflight
312
+ ? await options.preflight({ ...context, definition: fresh.definition, arguments: structuredClone(fresh.arguments) })
313
+ : { ok: false, reason: "canonical tool preflight is unavailable" };
314
+ if (!preflight.ok)
315
+ return terminalizeClaimed(options.approvalStore, decided, "drifted", preflight.reason);
289
316
  const attempted = options.approvalStore.markAttempted({
290
317
  approvalId: decided.approvalId,
291
318
  ownerId: decided.ownerId,
@@ -299,22 +326,28 @@ async function executeApprovalDecision(options) {
299
326
  catch (error) {
300
327
  if (!(error instanceof ApprovalExecutionFailedError))
301
328
  throw error;
302
- await options.hooks?.afterHandler?.();
303
- return options.approvalStore.complete({
304
- approvalId: attempted.approvalId,
305
- ownerId: attempted.ownerId,
306
- epoch: attempted.epoch,
307
- state: "failed",
308
- result: `error: ${error.message}`,
309
- });
329
+ result = { kind: "handler_failed", text: `error: ${error.message}`, error };
310
330
  }
331
+ if (!result || typeof result.text !== "string"
332
+ || !["rejected_before_handler", "handler_succeeded", "handler_failed", "handler_indeterminate"].includes(result.kind)) {
333
+ throw new ApprovalExecutionIndeterminateError("executor returned an invalid typed outcome; action was not retried");
334
+ }
335
+ const state = result.kind === "handler_succeeded" ? "succeeded"
336
+ : result.kind === "handler_indeterminate" ? "attempted_indeterminate" : "failed";
337
+ const emptyText = {
338
+ rejected_before_handler: "tool preflight rejected execution",
339
+ handler_succeeded: "action completed without output",
340
+ handler_failed: "action failed without details",
341
+ handler_indeterminate: "execution outcome is indeterminate; action was not retried",
342
+ };
343
+ const text = result.text.trim() ? result.text : emptyText[result.kind];
311
344
  await options.hooks?.afterHandler?.();
312
345
  return options.approvalStore.complete({
313
346
  approvalId: attempted.approvalId,
314
347
  ownerId: attempted.ownerId,
315
348
  epoch: attempted.epoch,
316
- state: "succeeded",
317
- result,
349
+ state,
350
+ result: result.kind === "rejected_before_handler" ? `no action was taken: ${text}` : text,
318
351
  });
319
352
  }
320
353
  function recoverClaimedApproval(options) {
@@ -49,8 +49,8 @@ const node_crypto_1 = require("node:crypto");
49
49
  const identity_1 = require("../heart/identity");
50
50
  const session_events_1 = require("../heart/session-events");
51
51
  const runtime_1 = require("../nerves/runtime");
52
- function getPendingDir(agentName, friendId, channel, key) {
53
- return path.join((0, identity_1.getAgentRoot)(agentName), "state", "pending", friendId, channel, key);
52
+ function getPendingDir(agentName, friendId, channel, key, agentRoot = (0, identity_1.getAgentRoot)(agentName)) {
53
+ return path.join(agentRoot, "state", "pending", friendId, channel, key);
54
54
  }
55
55
  function getDeferredReturnDir(agentName, friendId) {
56
56
  return path.join((0, identity_1.getAgentRoot)(agentName), "state", "pending-returns", friendId);
@@ -58,8 +58,8 @@ function getDeferredReturnDir(agentName, friendId) {
58
58
  /** Canonical private-runtime pending path segments. The path is historical durable state. */
59
59
  exports.PRIVATE_RUNTIME_PENDING = { friendId: "self", channel: "inner", key: "dialog" };
60
60
  /** Returns the pending dir for this agent's private runtime. */
61
- function getPrivateRuntimePendingDir(agentName) {
62
- return getPendingDir(agentName, exports.PRIVATE_RUNTIME_PENDING.friendId, exports.PRIVATE_RUNTIME_PENDING.channel, exports.PRIVATE_RUNTIME_PENDING.key);
61
+ function getPrivateRuntimePendingDir(agentName, agentRoot) {
62
+ return getPendingDir(agentName, exports.PRIVATE_RUNTIME_PENDING.friendId, exports.PRIVATE_RUNTIME_PENDING.channel, exports.PRIVATE_RUNTIME_PENDING.key, agentRoot);
63
63
  }
64
64
  function hasPendingMessages(pendingDir) {
65
65
  if (!fs.existsSync(pendingDir))