@ouro.bot/cli 0.1.0-alpha.742 → 0.1.0-alpha.743

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 (71) hide show
  1. package/changelog.json +26 -0
  2. package/deploy/unraid/Dockerfile +7 -2
  3. package/deploy/unraid/README.txt +704 -64
  4. package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
  5. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  6. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  7. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  8. package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
  9. package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
  10. package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
  13. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
  14. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
  15. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
  16. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
  17. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
  18. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
  19. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  20. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
  21. package/deploy/unraid/sanctuary.xml +13 -10
  22. package/dist/arc/cares.js +171 -37
  23. package/dist/arc/obligations.js +38 -0
  24. package/dist/heart/awaiting/await-alert.js +4 -2
  25. package/dist/heart/awaiting/await-expiry.js +39 -4
  26. package/dist/heart/awaiting/await-parser.js +8 -0
  27. package/dist/heart/core.js +37 -7
  28. package/dist/heart/cross-chat-delivery.js +1 -0
  29. package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
  30. package/dist/heart/daemon/container-spec-auditor.js +38 -11
  31. package/dist/heart/daemon/daemon-entry.js +19 -5
  32. package/dist/heart/daemon/daemon.js +154 -22
  33. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +126 -42
  34. package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
  35. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
  36. package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
  37. package/dist/heart/external-events/router.js +1025 -43
  38. package/dist/heart/session-events.js +23 -5
  39. package/dist/heart/steward-policy.js +302 -0
  40. package/dist/heart/tool-approval.js +3 -1
  41. package/dist/heart/turn-context.js +1 -0
  42. package/dist/mind/context.js +8 -3
  43. package/dist/mind/pending.js +25 -0
  44. package/dist/mind/prompt.js +49 -1
  45. package/dist/repertoire/guardrails.js +2 -1
  46. package/dist/repertoire/relationship-authorization.js +194 -0
  47. package/dist/repertoire/tools-awaiting.js +229 -26
  48. package/dist/repertoire/tools-base.js +15 -0
  49. package/dist/repertoire/tools-continuity.js +259 -4
  50. package/dist/repertoire/tools-notes.js +29 -3
  51. package/dist/repertoire/tools-session.js +219 -4
  52. package/dist/repertoire/tools-steward-policy.js +88 -0
  53. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  54. package/dist/repertoire/tools-unraid.js +113 -6
  55. package/dist/repertoire/tools.js +104 -9
  56. package/dist/repertoire/unraid-restart.js +86 -4
  57. package/dist/senses/private-runtime-worker.js +36 -3
  58. package/dist/senses/private-runtime.js +310 -63
  59. package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
  60. package/dist/senses/sanctuary-health-runner.js +47 -103
  61. package/dist/senses/sanctuary-health.js +53 -105
  62. package/dist/senses/sanctuary-runtime.js +36 -2
  63. package/dist/senses/sanctuary-sab.js +80 -0
  64. package/dist/senses/shared-turn.js +64 -9
  65. package/dist/senses/telegram-admission.js +684 -0
  66. package/dist/senses/telegram-approval-runtime.js +46 -6
  67. package/dist/senses/telegram-client.js +212 -66
  68. package/dist/senses/telegram-effect-adapter.js +787 -0
  69. package/dist/senses/telegram.js +728 -48
  70. package/npm-shrinkwrap.json +6 -6
  71. package/package.json +2 -2
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.surfaceToolDef = exports.speakTool = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
4
4
  exports.resetMcpDefinitions = resetMcpDefinitions;
5
+ exports.getSanctuaryRelationshipTools = getSanctuaryRelationshipTools;
5
6
  exports.getToolsForChannel = getToolsForChannel;
6
7
  exports.resolveToolDefinition = resolveToolDefinition;
7
8
  exports.approvalPolicyForToolName = approvalPolicyForToolName;
9
+ exports.classifyApprovalForInvocation = classifyApprovalForInvocation;
10
+ exports.approvalPolicyForInvocation = approvalPolicyForInvocation;
8
11
  exports.shellRiskProfile = shellRiskProfile;
9
12
  exports.riskProfileForTool = riskProfileForTool;
10
13
  exports.riskProfileForToolName = riskProfileForToolName;
@@ -27,6 +30,8 @@ const tools_voice_1 = require("./tools-voice");
27
30
  const shell_sessions_1 = require("./shell-sessions");
28
31
  const tools_unraid_1 = require("./tools-unraid");
29
32
  const tools_flow_1 = require("./tools-flow");
33
+ const tools_steward_policy_1 = require("./tools-steward-policy");
34
+ const steward_policy_1 = require("../heart/steward-policy");
30
35
  function safeGetAgentRoot() {
31
36
  try {
32
37
  return (0, identity_1.getAgentRoot)();
@@ -43,6 +48,33 @@ function isSanctuaryAgent() {
43
48
  return false;
44
49
  }
45
50
  }
51
+ const SANCTUARY_RELATIONSHIP_BASE_TOOLS = new Set([
52
+ "external_event_disposition",
53
+ "query_active_work",
54
+ "save_friend_note",
55
+ "telegram_contact_manage",
56
+ "query_cares",
57
+ "care_manage",
58
+ "await_condition",
59
+ "resolve_await",
60
+ "cancel_await",
61
+ "send_message",
62
+ ]);
63
+ const SANCTUARY_TELEGRAM_SURFACE = new Set([
64
+ "query_active_work",
65
+ "save_friend_note",
66
+ "telegram_contact_manage",
67
+ "query_cares",
68
+ "care_manage",
69
+ "await_condition",
70
+ "resolve_await",
71
+ "cancel_await",
72
+ ...tools_unraid_1.unraidToolDefinitions.map((definition) => definition.tool.function.name),
73
+ "steward_policy_manage",
74
+ "ponder",
75
+ "settle",
76
+ "speak",
77
+ ]);
46
78
  // Re-export types and constants used by the rest of the codebase
47
79
  var tools_base_2 = require("./tools-base");
48
80
  Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_base_2.tools; } });
@@ -55,7 +87,7 @@ Object.defineProperty(exports, "speakTool", { enumerable: true, get: function ()
55
87
  var tools_surface_2 = require("./tools-surface");
56
88
  Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: function () { return tools_surface_2.surfaceToolDef; } });
57
89
  // All tool definitions in a single registry
58
- const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, ...tools_unraid_1.unraidToolDefinitions, tools_surface_1.surfaceToolDefinition];
90
+ const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, ...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, tools_surface_1.surfaceToolDefinition];
59
91
  const COMMERCE_AUTHORITY_TOOLS = new Set(["stripe_create_card", "flight_hold", "flight_book"]);
60
92
  // MCP tool definitions — populated each time getToolsForChannel() is called with an mcpManager.
61
93
  // Kept separate from allDefinitions so execTool can find them.
@@ -68,6 +100,32 @@ function baseToolsForCapabilities() {
68
100
  // Use baseToolDefinitions at call time so dynamically-added tools are included
69
101
  return tools_base_1.baseToolDefinitions.map((d) => d.tool);
70
102
  }
103
+ function uniqueToolSchemas(tools) {
104
+ const seen = new Set();
105
+ return tools.filter((tool) => {
106
+ if (seen.has(tool.function.name))
107
+ return false;
108
+ seen.add(tool.function.name);
109
+ return true;
110
+ });
111
+ }
112
+ /**
113
+ * Resolve Sanctuary relationship tools from one canonical definition pool.
114
+ * Durable relationship profiles can only reduce this surface; they cannot
115
+ * introduce an arbitrary base tool by naming it in configuration.
116
+ */
117
+ function getSanctuaryRelationshipTools(advertisedToolNames) {
118
+ const advertised = new Set(advertisedToolNames);
119
+ return uniqueToolSchemas([
120
+ ...tools_base_1.baseToolDefinitions.filter((definition) => SANCTUARY_RELATIONSHIP_BASE_TOOLS.has(definition.tool.function.name)).map((definition) => definition.tool),
121
+ ...tools_unraid_1.unraidToolDefinitions.map((definition) => definition.tool),
122
+ tools_steward_policy_1.stewardPolicyToolDefinition.tool,
123
+ tools_flow_1.ponderTool,
124
+ tools_flow_1.restTool,
125
+ tools_flow_1.settleTool,
126
+ tools_flow_1.speakTool,
127
+ ]).filter((tool) => advertised.has(tool.function.name));
128
+ }
71
129
  // Apply a single tool preference to a tool schema, returning a new object.
72
130
  function applyPreference(tool, pref) {
73
131
  return {
@@ -96,12 +154,7 @@ function filterByCapability(toolList, providerCapabilities) {
96
154
  // When providerCapabilities is provided, tools with requiredCapability are filtered.
97
155
  function getToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities, mcpManager, _chatModel) {
98
156
  if (capabilities?.channel === "telegram" && isSanctuaryAgent()) {
99
- return [
100
- ...tools_unraid_1.unraidToolDefinitions.map((definition) => definition.tool),
101
- tools_flow_1.ponderTool,
102
- tools_flow_1.settleTool,
103
- tools_flow_1.speakTool,
104
- ];
157
+ return getSanctuaryRelationshipTools([...SANCTUARY_TELEGRAM_SURFACE]);
105
158
  }
106
159
  const baseTools = baseToolsForCapabilities();
107
160
  const bluebubblesTools = capabilities?.channel === "bluebubbles"
@@ -156,6 +209,36 @@ function resolveToolDefinition(toolName) {
156
209
  function approvalPolicyForToolName(name, args) {
157
210
  return resolveToolDefinition(name)?.approvalPolicy?.(args) ?? { kind: "not_required" };
158
211
  }
212
+ async function routineActionInvocation(name, args, ctx) {
213
+ const target = typeof args.container === "string" ? args.container : "";
214
+ if (name !== "unraid_restart_container" || !ctx?.agentRoot || !(0, tools_base_1.routineActionRequester)(ctx) || !target)
215
+ return null;
216
+ const relationshipAuthorization = ctx.relationshipAuthorization;
217
+ if (!relationshipAuthorization)
218
+ return null;
219
+ let authorization;
220
+ try {
221
+ authorization = await relationshipAuthorization.authorizeTool(name, args);
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ if (!authorization.allowed || !Number.isInteger(authorization.profileVersion) || Number(authorization.profileVersion) < 1)
227
+ return null;
228
+ const key = `unraid.restart:${target}`;
229
+ const decision = (0, steward_policy_1.inspectRoutineActionGrant)(ctx.agentRoot, { key, action: "unraid.container.restart", target });
230
+ return decision.allowed ? { key, target, expectedPolicyVersion: decision.policyVersion } : null;
231
+ }
232
+ async function classifyApprovalForInvocation(name, args, ctx) {
233
+ const fallback = approvalPolicyForToolName(name, args);
234
+ if (name !== "unraid_restart_container" || fallback.kind !== "required")
235
+ return { policy: fallback };
236
+ const routineActionSelection = await routineActionInvocation(name, args, ctx);
237
+ return routineActionSelection ? { policy: { kind: "not_required" }, routineActionSelection } : { policy: fallback };
238
+ }
239
+ async function approvalPolicyForInvocation(name, args, ctx) {
240
+ return (await classifyApprovalForInvocation(name, args, ctx)).policy;
241
+ }
159
242
  const findDefinition = resolveToolDefinition;
160
243
  function normalizeGuardArgs(_name, args) {
161
244
  return args;
@@ -267,6 +350,17 @@ async function execTool(name, args, ctx) {
267
350
  });
268
351
  return `unknown: ${name}`;
269
352
  }
353
+ const relationshipDecision = await ctx?.relationshipAuthorization?.authorizeTool(name, args);
354
+ if (relationshipDecision && !relationshipDecision.allowed) {
355
+ (0, runtime_1.emitNervesEvent)({
356
+ level: "warn",
357
+ event: "tool.relationship_authorization_block",
358
+ component: "tools",
359
+ message: "relationship authorization blocked tool execution",
360
+ meta: { name, reason: relationshipDecision.reason },
361
+ });
362
+ return `relationship authorization required: ${relationshipDecision.reason}`;
363
+ }
270
364
  const riskProfile = riskProfileForTool(def, name, args);
271
365
  const orientationPolicy = ctx?.orientationFrame?.actionPolicy;
272
366
  if (orientationPolicy?.mode === "correction_hold"
@@ -323,16 +417,17 @@ async function execTool(name, args, ctx) {
323
417
  });
324
418
  return `commerce authority required: ${commerceReservation.reason}`;
325
419
  }
420
+ const authorizedContext = ctx;
326
421
  const toolContext = commerceReservation?.ok
327
422
  ? {
328
- ...ctx,
423
+ ...authorizedContext,
329
424
  agentRoot: guardContext.agentRoot,
330
425
  commerceAuthority: {
331
426
  checkoutId: commerceReservation.checkoutId,
332
427
  reservationToken: commerceReservation.reservationToken,
333
428
  },
334
429
  }
335
- : ctx;
430
+ : authorizedContext;
336
431
  try {
337
432
  const result = await def.handler(args, toolContext);
338
433
  (0, runtime_1.emitNervesEvent)({
@@ -8,6 +8,12 @@ const unraid_client_1 = require("./unraid-client");
8
8
  exports.SANCTUARY_RESTART_MUTATION = `mutation SanctuaryRestart($id: PrefixedID!) {
9
9
  docker { restart(id: $id) { id names state status autoStart } }
10
10
  }`;
11
+ class RoutineActionReceiptError extends Error {
12
+ constructor(cause) {
13
+ super("routine action receipt persistence failed", { cause });
14
+ this.name = "RoutineActionReceiptError";
15
+ }
16
+ }
11
17
  function failure(code, message) {
12
18
  return { ok: false, error: { code, message, degraded: true } };
13
19
  }
@@ -40,7 +46,15 @@ function createApprovedUnraidRestartExecutor(options) {
40
46
  const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
41
47
  const now = options.now ?? (() => new Date());
42
48
  const observationTimeoutMs = options.observationTimeoutMs ?? 30_000;
43
- return async (args) => {
49
+ const transitionRoutine = (input) => {
50
+ try {
51
+ options.transitionRoutineAction?.(input);
52
+ }
53
+ catch (error) {
54
+ throw new RoutineActionReceiptError(error);
55
+ }
56
+ };
57
+ return async (args, execution) => {
44
58
  if (!validArgument(args.container))
45
59
  return failure("invalid_response", "container must be one bounded exact name");
46
60
  const resolved = exactTarget(await options.listContainers(), args.container);
@@ -51,6 +65,22 @@ function createApprovedUnraidRestartExecutor(options) {
51
65
  return fresh;
52
66
  if (fresh.id !== resolved.id)
53
67
  return failure("stale_target", "container identity changed before restart");
68
+ let routineAuthorization = null;
69
+ if (execution?.routine) {
70
+ let authorization;
71
+ try {
72
+ authorization = await execution.routine.reauthorize();
73
+ }
74
+ catch {
75
+ return failure("stale_target", "routine relationship authorization is unavailable");
76
+ }
77
+ if (!authorization.allowed)
78
+ return failure("stale_target", authorization.reason);
79
+ if (!authorization.receiptId.trim() || !Number.isInteger(authorization.profileVersion) || authorization.profileVersion < 1) {
80
+ return failure("stale_target", "routine relationship authorization is not versioned");
81
+ }
82
+ routineAuthorization = { receiptId: authorization.receiptId, profileVersion: authorization.profileVersion };
83
+ }
54
84
  const scenarioHandleDigest = options.acceptanceScenarioHandleDigest?.();
55
85
  const approval = options.acceptanceApproval?.();
56
86
  const attempt = {
@@ -65,13 +95,44 @@ function createApprovedUnraidRestartExecutor(options) {
65
95
  mutationAcknowledged: false,
66
96
  afterState: null,
67
97
  };
98
+ let routineReceipt = null;
99
+ let routineState = null;
100
+ if (execution?.routine) {
101
+ if (!options.reserveRoutineAction || !options.transitionRoutineAction)
102
+ return failure("invalid_response", "routine action ledger is unavailable");
103
+ try {
104
+ routineReceipt = options.reserveRoutineAction({
105
+ key: execution.routine.key,
106
+ expectedPolicyVersion: execution.routine.expectedPolicyVersion,
107
+ authorizationReceiptId: routineAuthorization.receiptId,
108
+ authorizationVersion: routineAuthorization.profileVersion,
109
+ action: "unraid.container.restart",
110
+ target: fresh.name,
111
+ attemptId: attempt.attemptId,
112
+ expectedBeforeState: fresh.state,
113
+ resolvedTarget: { id: fresh.id, name: fresh.name },
114
+ effect: { operation: "restart", targetId: fresh.id },
115
+ });
116
+ routineState = "reserved";
117
+ }
118
+ catch (error) {
119
+ return failure("stale_target", error instanceof Error ? error.message : "routine action authority changed");
120
+ }
121
+ }
68
122
  if (approval && approval.argumentDigest !== attempt.argumentDigest)
69
123
  return failure("stale_target", "approval arguments changed before restart");
70
124
  await options.persistAttempt?.({ ...attempt, observedAt: now().toISOString(), state: "attempt_not_started" });
71
125
  const apiKey = await options.loadWriteApiKey();
72
- if (!apiKey.trim())
126
+ if (!apiKey.trim()) {
127
+ if (routineReceipt && routineState === "reserved")
128
+ transitionRoutine({ id: routineReceipt.id, expectedState: "reserved", state: "failed", recoveryState: { state: "completed", compensation: "none" } });
73
129
  return failure("invalid_response", "Unraid write credential is unavailable");
130
+ }
74
131
  const client = createClient({ endpoint: options.endpoint, apiKey });
132
+ if (routineReceipt && routineState === "reserved") {
133
+ transitionRoutine({ id: routineReceipt.id, expectedState: "reserved", state: "attempting" });
134
+ routineState = "attempting";
135
+ }
75
136
  await options.persistAttempt?.({ ...attempt, observedAt: now().toISOString(), state: "attempting" });
76
137
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.unraid_restart_start", message: "approved Unraid restart started", meta: { containerId: fresh.id, containerName: fresh.name } });
77
138
  const persistTerminal = async (terminal) => {
@@ -85,8 +146,9 @@ function createApprovedUnraidRestartExecutor(options) {
85
146
  }
86
147
  };
87
148
  let acknowledged = false;
149
+ let mutation = null;
88
150
  try {
89
- const mutation = await client.mutate(exports.SANCTUARY_RESTART_MUTATION, { id: fresh.id });
151
+ mutation = await client.mutate(exports.SANCTUARY_RESTART_MUTATION, { id: fresh.id });
90
152
  acknowledged = acknowledgedIdentity(mutation, fresh);
91
153
  if (!acknowledged)
92
154
  throw new Error("restart response identity was invalid");
@@ -94,6 +156,10 @@ function createApprovedUnraidRestartExecutor(options) {
94
156
  catch {
95
157
  acknowledged = false;
96
158
  }
159
+ if (acknowledged && mutation && routineReceipt && routineState === "attempting") {
160
+ transitionRoutine({ id: routineReceipt.id, expectedState: "attempting", state: "effect_acknowledged", effectReceipt: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(mutation)).digest("hex") });
161
+ routineState = "effect_acknowledged";
162
+ }
97
163
  const startedAt = now().getTime();
98
164
  let sawRestarting = false;
99
165
  try {
@@ -102,6 +168,8 @@ function createApprovedUnraidRestartExecutor(options) {
102
168
  if (!("ok" in observed)) {
103
169
  if (observed.id !== fresh.id) {
104
170
  await persistTerminal({ ...attempt, observedAt: now().toISOString(), state: "attempted_or_indeterminate", mutationAcknowledged: acknowledged });
171
+ if (routineReceipt && routineState)
172
+ transitionRoutine({ id: routineReceipt.id, expectedState: routineState, state: "indeterminate", recoveryState: { state: "manual_inspection_required", compensation: "none" } });
105
173
  return failure("ambiguous", "container identity changed after restart attempt");
106
174
  }
107
175
  sawRestarting ||= observed.state === "restarting";
@@ -109,6 +177,14 @@ function createApprovedUnraidRestartExecutor(options) {
109
177
  if (!await persistTerminal({ ...attempt, observedAt: now().toISOString(), state: "succeeded", mutationAcknowledged: acknowledged, afterState: observed.state })) {
110
178
  return failure("ambiguous", "restart succeeded but its terminal receipt could not be persisted; it was not retried");
111
179
  }
180
+ if (routineReceipt && routineState === "attempting") {
181
+ transitionRoutine({ id: routineReceipt.id, expectedState: "attempting", state: "effect_acknowledged", effectReceipt: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify({ observation: "restarting", target: fresh.id })).digest("hex") });
182
+ routineState = "effect_acknowledged";
183
+ }
184
+ if (routineReceipt && routineState === "effect_acknowledged") {
185
+ transitionRoutine({ id: routineReceipt.id, expectedState: "effect_acknowledged", state: "verified", verifiedAfterState: observed.state, recoveryState: { state: "completed", compensation: "none" } });
186
+ routineState = null;
187
+ }
112
188
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.unraid_restart_end", message: "approved Unraid restart completed", meta: { containerId: fresh.id, observedRestart: true } });
113
189
  return { ok: true, data: { container: { id: fresh.id, name: fresh.name }, beforeState: fresh.state, afterState: observed.state, observedRestart: true, degraded: false } };
114
190
  }
@@ -118,12 +194,18 @@ function createApprovedUnraidRestartExecutor(options) {
118
194
  await sleep(1_000);
119
195
  } while (true);
120
196
  }
121
- catch {
197
+ catch (error) {
198
+ if (error instanceof RoutineActionReceiptError)
199
+ throw error;
122
200
  await persistTerminal({ ...attempt, observedAt: now().toISOString(), state: "attempted_or_indeterminate", mutationAcknowledged: acknowledged });
201
+ if (routineReceipt && routineState)
202
+ transitionRoutine({ id: routineReceipt.id, expectedState: routineState, state: "indeterminate", recoveryState: { state: "manual_inspection_required", compensation: "none" } });
123
203
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "repertoire", event: "repertoire.unraid_restart_error", message: "approved Unraid restart observation failed", meta: { containerId: fresh.id, acknowledged } });
124
204
  return failure("ambiguous", "restart was attempted but observation failed; it was not retried");
125
205
  }
126
206
  await persistTerminal({ ...attempt, observedAt: now().toISOString(), state: "attempted_or_indeterminate", mutationAcknowledged: acknowledged });
207
+ if (routineReceipt && routineState)
208
+ transitionRoutine({ id: routineReceipt.id, expectedState: routineState, state: "indeterminate", recoveryState: { state: "manual_inspection_required", compensation: "none" } });
127
209
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "repertoire", event: "repertoire.unraid_restart_error", message: "approved Unraid restart outcome was ambiguous", meta: { containerId: fresh.id, acknowledged } });
128
210
  return failure("ambiguous", "restart was attempted but could not be proven; it was not retried");
129
211
  };
@@ -39,6 +39,7 @@ exports.startPrivateRuntimeWorker = startPrivateRuntimeWorker;
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const private_runtime_1 = require("./private-runtime");
42
+ const router_1 = require("../heart/external-events/router");
42
43
  const runtime_1 = require("../nerves/runtime");
43
44
  const identity_1 = require("../heart/identity");
44
45
  const pending_1 = require("../mind/pending");
@@ -471,9 +472,29 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
471
472
  });
472
473
  }
473
474
  }
474
- async function run(reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend) {
475
+ const startExternalEventHeartbeat = (event) => {
476
+ const renew = () => {
477
+ for (const member of [event, ...(event.relatedEvents ?? [])]) {
478
+ try {
479
+ (0, router_1.renewExternalEventClaim)(member.recordPath, { owner: member.claimOwner, expectedGeneration: member.generation, leaseMs: 30_000 });
480
+ }
481
+ catch (error) {
482
+ (0, runtime_1.emitNervesEvent)({
483
+ level: "warn", component: "senses", event: "senses.external_event_lease_renew_error",
484
+ message: "external-event turn lease renewal failed",
485
+ meta: { recordPath: member.recordPath, generation: member.generation, error: error instanceof Error ? error.message : String(error) },
486
+ });
487
+ }
488
+ }
489
+ };
490
+ renew();
491
+ const timer = setInterval(renew, 10_000);
492
+ timer.unref?.();
493
+ return () => clearInterval(timer);
494
+ };
495
+ async function run(reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend, externalEvent) {
475
496
  if (running) {
476
- queue.push({ reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend });
497
+ queue.push({ reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend, externalEvent, ...(externalEvent ? { stopExternalEventHeartbeat: startExternalEventHeartbeat(externalEvent) } : {}) });
477
498
  return;
478
499
  }
479
500
  running = true;
@@ -484,6 +505,8 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
484
505
  let nextAwaitName = awaitName;
485
506
  let nextTrigger = trigger;
486
507
  let nextPrivateTurnDecision = privateTurnDecision;
508
+ let nextExternalEvent = externalEvent;
509
+ let nextExternalEventHeartbeat = externalEvent ? startExternalEventHeartbeat(externalEvent) : undefined;
487
510
  let nextNoSend = reason === "habit" ? noSend : undefined;
488
511
  let nextHabitRun = null;
489
512
  let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
@@ -492,6 +515,8 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
492
515
  const currentHabitName = nextHabitName;
493
516
  const currentTrigger = nextTrigger ?? "overdue";
494
517
  const currentNoSend = nextNoSend;
518
+ const currentExternalEventHeartbeat = nextExternalEventHeartbeat;
519
+ nextExternalEventHeartbeat = undefined;
495
520
  const currentHabitRun = currentReason === "habit" && currentHabitName
496
521
  ? nextHabitRun && nextHabitRun.habit.name === currentHabitName
497
522
  ? nextHabitRun
@@ -545,6 +570,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
545
570
  awaitName: nextAwaitName,
546
571
  ...(currentNoSend ? { noSend: true } : {}),
547
572
  ...(nextPrivateTurnDecision ? { privateTurnDecision: nextPrivateTurnDecision } : {}),
573
+ ...(nextExternalEvent ? { externalEvent: nextExternalEvent } : {}),
548
574
  ...(currentHabitRun
549
575
  ? {
550
576
  trigger: currentHabitRun.trigger,
@@ -587,6 +613,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
587
613
  },
588
614
  });
589
615
  }
616
+ currentExternalEventHeartbeat?.();
590
617
  if (currentReason === "habit" && currentHabitName === "heartbeat") {
591
618
  heartbeatOkRestedAt = isHeartbeatOkRestResult(turnResult) ? nowSource() : null;
592
619
  }
@@ -613,6 +640,8 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
613
640
  nextAwaitName = next.awaitName;
614
641
  nextTrigger = next.trigger;
615
642
  nextPrivateTurnDecision = next.privateTurnDecision;
643
+ nextExternalEvent = next.externalEvent;
644
+ nextExternalEventHeartbeat = next.stopExternalEventHeartbeat;
616
645
  nextNoSend = next.reason === "habit" ? next.noSend : undefined;
617
646
  consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
618
647
  continue runLoop;
@@ -650,6 +679,8 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
650
679
  nextAwaitName = undefined;
651
680
  nextTrigger = currentTrigger;
652
681
  nextPrivateTurnDecision = undefined;
682
+ nextExternalEvent = undefined;
683
+ nextExternalEventHeartbeat = undefined;
653
684
  nextNoSend = currentNoSend;
654
685
  nextHabitRun = currentHabitRun;
655
686
  }
@@ -662,6 +693,8 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
662
693
  nextAwaitName = undefined;
663
694
  nextTrigger = undefined;
664
695
  nextPrivateTurnDecision = undefined;
696
+ nextExternalEvent = undefined;
697
+ nextExternalEventHeartbeat = undefined;
665
698
  nextNoSend = undefined;
666
699
  }
667
700
  continue;
@@ -715,7 +748,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
715
748
  if (maybeMessage.type === "chat" ||
716
749
  maybeMessage.type === "message") {
717
750
  clearHeartbeatRestShield();
718
- await run("instinct", undefined, undefined, undefined, undefined, maybeMessage.privateTurnDecision);
751
+ await run("instinct", undefined, undefined, undefined, undefined, maybeMessage.privateTurnDecision, undefined, maybeMessage.externalEvent);
719
752
  return;
720
753
  }
721
754
  if (maybeMessage.type === "shutdown") {