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

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 +32 -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
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.renderRelationshipPreferences = renderRelationshipPreferences;
37
+ exports.withRelationshipPreference = withRelationshipPreference;
38
+ exports.loadRelationshipCapabilityRegistry = loadRelationshipCapabilityRegistry;
39
+ exports.relationshipSubjectFromFriend = relationshipSubjectFromFriend;
40
+ exports.createRelationshipAuthorizationEvaluator = createRelationshipAuthorizationEvaluator;
41
+ exports.resolveProfileScopedRelationshipAuthorization = resolveProfileScopedRelationshipAuthorization;
42
+ exports.authorizeRelationshipAccess = authorizeRelationshipAccess;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const node_crypto_1 = require("node:crypto");
46
+ const runtime_1 = require("../nerves/runtime");
47
+ function renderRelationshipPreferences(friend) {
48
+ const now = Date.now();
49
+ return Object.entries(friend.relationshipPolicy?.preferences ?? {})
50
+ .filter(([, preference]) => !preference.expiresAt || Date.parse(preference.expiresAt) > now)
51
+ .slice(0, 20)
52
+ .map(([category, preference]) => `- source=${preference.source}; provenance=${preference.provenance}; category=${category}; value=${String(preference.value).replace(/\s+/gu, " ").trim().slice(0, 500)}; version=${preference.version}; precedence=current${preference.expiresAt ? `; expires=${preference.expiresAt}` : ""}`);
53
+ }
54
+ function withRelationshipPreference(friend, input) {
55
+ const current = friend.relationshipPolicy ?? { schemaVersion: 1, version: 0, preferences: {} };
56
+ const previous = current.preferences[input.category];
57
+ return {
58
+ ...friend,
59
+ relationshipPolicy: {
60
+ schemaVersion: 1,
61
+ version: current.version + 1,
62
+ preferences: { ...current.preferences, [input.category]: { value: input.value, provenance: input.provenance, source: input.source, version: (previous?.version ?? 0) + 1 } },
63
+ },
64
+ updatedAt: new Date().toISOString(),
65
+ };
66
+ }
67
+ const deny = (reason) => ({ allowed: false, reason });
68
+ function authorizationReceipt(value) {
69
+ return `relationship-${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex")}`;
70
+ }
71
+ function stringList(value, label) {
72
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim()))
73
+ throw new Error(`${label} must be a string array`);
74
+ return [...new Set(value)];
75
+ }
76
+ function loadRelationshipCapabilityRegistry(agentRoot) {
77
+ const filePath = path.join(agentRoot, "tool-profiles.json");
78
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
79
+ if (raw.version !== 2 || !raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles))
80
+ throw new Error("relationship capability registry version 2 is required");
81
+ const profiles = {};
82
+ for (const [id, value] of Object.entries(raw.profiles)) {
83
+ if (!value || typeof value !== "object" || Array.isArray(value))
84
+ throw new Error(`relationship capability profile ${id} is invalid`);
85
+ const candidate = value;
86
+ if (!Number.isInteger(candidate.version) || Number(candidate.version) < 1)
87
+ throw new Error(`relationship capability profile ${id} version is invalid`);
88
+ profiles[id] = {
89
+ id,
90
+ version: Number(candidate.version),
91
+ contextScopes: stringList(candidate.contextScopes, `${id}.contextScopes`),
92
+ toolNames: stringList(candidate.toolNames, `${id}.toolNames`),
93
+ effectScopes: stringList(candidate.effectScopes, `${id}.effectScopes`),
94
+ };
95
+ }
96
+ return { version: 2, profiles };
97
+ }
98
+ function relationshipSubjectFromFriend(friend) {
99
+ return {
100
+ friendId: friend.id,
101
+ trustLevel: friend.trustLevel ?? "stranger",
102
+ admissionState: friend.admissionState ?? "unverified",
103
+ initiativePolicy: friend.initiativePolicy ?? "none",
104
+ ...(friend.capabilityProfileId ? { capabilityProfileId: friend.capabilityProfileId } : {}),
105
+ };
106
+ }
107
+ function createRelationshipAuthorizationEvaluator(input) {
108
+ const subject = relationshipSubjectFromFriend(input.friend);
109
+ const relationshipProfile = subject.capabilityProfileId ? input.registry.profiles[subject.capabilityProfileId] : undefined;
110
+ const turnProfile = input.profileId ? input.registry.profiles[input.profileId] : relationshipProfile;
111
+ const profile = relationshipProfile && turnProfile
112
+ ? {
113
+ id: turnProfile.id,
114
+ version: turnProfile.version,
115
+ contextScopes: turnProfile.contextScopes.filter((scope) => relationshipProfile.contextScopes.includes(scope)),
116
+ toolNames: turnProfile.toolNames.filter((name) => relationshipProfile.toolNames.includes(name)),
117
+ effectScopes: turnProfile.effectScopes.filter((scope) => relationshipProfile.effectScopes.includes(scope)),
118
+ }
119
+ : undefined;
120
+ const effectiveSubject = { ...subject, ...(profile ? { capabilityProfileId: profile.id } : {}) };
121
+ const profiles = profile ? [profile] : [];
122
+ const evaluate = (request) => authorizeRelationshipAccess({
123
+ relationship: effectiveSubject,
124
+ profiles,
125
+ request,
126
+ activeRequestId: input.requestId,
127
+ requestPhase: input.requestPhase,
128
+ });
129
+ const advertisedToolNames = profile?.toolNames.filter((name) => evaluate({ kind: "tool", name, ...(input.requestId ? { requestId: input.requestId, returnTargetFriendId: subject.friendId } : {}) }).allowed) ?? [];
130
+ const authorizedContextScopes = profile?.contextScopes.filter((scope) => evaluate({ kind: "context", scope }).allowed) ?? [];
131
+ return {
132
+ subject,
133
+ ...(profile ? { profileId: profile.id } : {}),
134
+ authorizedContextScopes,
135
+ advertisedToolNames,
136
+ ...(input.sessionEventId ? { actor: { friendId: subject.friendId, trustLevel: subject.trustLevel, sessionEventId: input.sessionEventId } } : {}),
137
+ authorizeContext: (scope) => evaluate({ kind: "context", scope }),
138
+ authorizeTool: (name) => evaluate({ kind: "tool", name, ...(input.requestId ? { requestId: input.requestId, returnTargetFriendId: subject.friendId } : {}) }),
139
+ authorizeEffect: (scope) => evaluate({ kind: "effect", scope, ...(input.requestId ? { requestId: input.requestId, returnTargetFriendId: subject.friendId } : {}) }),
140
+ };
141
+ }
142
+ async function resolveProfileScopedRelationshipAuthorization(input) {
143
+ const friends = await input.store.listAll?.() ?? [];
144
+ const matches = friends.filter((friend) => friend.capabilityProfileId === input.relationshipProfileId);
145
+ if (matches.length !== 1)
146
+ throw new Error(`relationship profile ${input.relationshipProfileId} must resolve to exactly one Friend`);
147
+ return createRelationshipAuthorizationEvaluator({
148
+ friend: matches[0],
149
+ registry: input.registry,
150
+ ...(input.profileId ? { profileId: input.profileId } : {}),
151
+ ...(input.requestId ? { requestId: input.requestId } : {}),
152
+ ...(input.requestPhase ? { requestPhase: input.requestPhase } : {}),
153
+ ...(input.sessionEventId ? { sessionEventId: input.sessionEventId } : {}),
154
+ });
155
+ }
156
+ function authorizeRelationshipAccess(input) {
157
+ if (input.request.kind === "admission_gate") {
158
+ const pending = input.pendingAdmission;
159
+ const now = Date.parse(input.now ?? new Date().toISOString());
160
+ if (!pending || pending.admissionId !== input.request.admissionId || pending.botId !== input.request.botId || pending.userId !== input.request.userId || pending.chatId !== input.request.chatId || pending.expiresAt !== input.request.expiresAt || now >= Date.parse(input.request.expiresAt) || input.request.idempotencyKey !== `ack:${input.request.admissionId}`) {
161
+ return deny("admission gate authorization does not match a current pending admission");
162
+ }
163
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.relationship_authorized", message: "authorized fixed admission gate acknowledgement", meta: { authorizationKind: "admission_gate", admissionId: input.request.admissionId } });
164
+ return { allowed: true, authorizationKind: "admission_gate", receiptId: authorizationReceipt(input.request), admissionId: input.request.admissionId, idempotencyKey: input.request.idempotencyKey, expiresAt: input.request.expiresAt };
165
+ }
166
+ const relationship = input.relationship;
167
+ if (!relationship || relationship.admissionState !== "active")
168
+ return deny("relationship admission is not active");
169
+ if (relationship.trustLevel !== "friend" && relationship.trustLevel !== "family")
170
+ return deny("relationship trust is insufficient");
171
+ const profile = input.profiles.find((candidate) => candidate.id === relationship.capabilityProfileId);
172
+ if (!profile || !Number.isInteger(profile.version) || profile.version < 1)
173
+ return deny("relationship capability profile is missing or stale");
174
+ if (input.request.kind === "context" && !profile.contextScopes.includes(input.request.scope))
175
+ return deny("context scope is not authorized by the relationship profile");
176
+ if (input.request.kind === "tool" && !profile.toolNames.includes(input.request.name))
177
+ return deny("tool is not authorized by the relationship profile");
178
+ if (input.request.kind === "effect" && !profile.effectScopes.includes(input.request.scope))
179
+ return deny("effect is not authorized by the relationship profile");
180
+ const requestId = input.request.kind === "context" ? undefined : input.request.requestId;
181
+ const returnTarget = input.request.kind === "context" ? undefined : input.request.returnTargetFriendId;
182
+ if (relationship.initiativePolicy === "none")
183
+ return deny("initiative policy denies contact");
184
+ if (relationship.initiativePolicy === "reactive_only" && input.request.kind !== "context") {
185
+ if (input.requestPhase !== "inbound" || !requestId || requestId !== input.activeRequestId || returnTarget !== relationship.friendId)
186
+ return deny("initiative policy is reactive only");
187
+ }
188
+ if (relationship.initiativePolicy === "request_follow_up_only" && input.request.kind !== "context") {
189
+ if (!requestId || requestId !== input.activeRequestId || returnTarget !== relationship.friendId)
190
+ return deny("initiative policy requires the matching active request and return target");
191
+ }
192
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.relationship_authorized", message: "authorized relationship access", meta: { authorizationKind: "relationship", friendId: relationship.friendId, profileId: profile.id, profileVersion: profile.version, requestKind: input.request.kind } });
193
+ return { allowed: true, authorizationKind: "relationship", receiptId: authorizationReceipt({ relationship, request: input.request, activeRequestId: input.activeRequestId, requestPhase: input.requestPhase, profile }), friendId: relationship.friendId, profileId: profile.id, profileVersion: profile.version, requestId: requestId ?? null };
194
+ }
@@ -34,8 +34,16 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.awaitingToolDefinitions = void 0;
37
+ exports.readAwaitDefinition = readAwaitDefinition;
37
38
  exports.setAwaitToolDeps = setAwaitToolDeps;
38
39
  exports.resetAwaitToolDeps = resetAwaitToolDeps;
40
+ exports.cancelAwaitTool = cancelAwaitTool;
41
+ exports.cancelStaleAwait = cancelStaleAwait;
42
+ exports.inspectRelationshipFollowUp = inspectRelationshipFollowUp;
43
+ exports.hasActiveRelationshipFollowUp = hasActiveRelationshipFollowUp;
44
+ exports.hasActiveExternalEventAwait = hasActiveExternalEventAwait;
45
+ exports.inspectExternalEventAwait = inspectExternalEventAwait;
46
+ exports.cancelRelationshipFollowUps = cancelRelationshipFollowUps;
39
47
  const fs = __importStar(require("fs"));
40
48
  const path = __importStar(require("path"));
41
49
  const identity_1 = require("../heart/identity");
@@ -44,6 +52,9 @@ const runtime_1 = require("../nerves/runtime");
44
52
  const await_parser_1 = require("../heart/awaiting/await-parser");
45
53
  const await_alert_1 = require("../heart/awaiting/await-alert");
46
54
  const pending_1 = require("../mind/pending");
55
+ const router_1 = require("../heart/external-events/router");
56
+ const obligations_1 = require("../arc/obligations");
57
+ const cadence_1 = require("../heart/daemon/cadence");
47
58
  /**
48
59
  * Bundle-root-relative locations.
49
60
  * - `awaiting/<name>.md` — active awaits (status: pending)
@@ -108,7 +119,7 @@ function resolveDeliveryDeps(agentName) {
108
119
  return injected.buildDeliveryDeps(agentName);
109
120
  return defaultDeliveryDeps(agentName);
110
121
  }
111
- function fileAwait(args, agentRoot, agentName, sessionFriendId, sessionChannel) {
122
+ function fileAwait(args, agentRoot, agentName, sessionFriendId, sessionChannel, sessionKey, requestId) {
112
123
  const nameError = validateName(args.name);
113
124
  if (nameError)
114
125
  return JSON.stringify({ error: nameError });
@@ -118,26 +129,55 @@ function fileAwait(args, agentRoot, agentName, sessionFriendId, sessionChannel)
118
129
  if (!args.cadence || !args.cadence.trim()) {
119
130
  return JSON.stringify({ error: "cadence is required" });
120
131
  }
132
+ if (args.wake_at && (!Number.isFinite(Date.parse(args.wake_at)) || new Date(args.wake_at).toISOString() !== args.wake_at)) {
133
+ return JSON.stringify({ error: "wake_at must be a canonical ISO timestamp" });
134
+ }
121
135
  const filePath = awaitFilePath(agentRoot, args.name);
122
136
  if (fs.existsSync(filePath)) {
123
137
  return JSON.stringify({ error: `await "${args.name}" already exists` });
124
138
  }
125
139
  const mode = args.mode === "quick" ? "quick" : "full";
126
- const alert = args.alert ?? sessionChannel ?? null;
140
+ const alert = sessionChannel === "external-event" ? null : args.alert ?? sessionChannel ?? null;
127
141
  const frontmatter = {
128
142
  condition: (0, session_events_1.capStructuredRecordString)(args.condition.trim()),
129
143
  cadence: (0, session_events_1.capStructuredRecordString)(args.cadence.trim()),
130
144
  alert,
131
145
  mode,
132
146
  max_age: typeof args.max_age === "string" ? (0, session_events_1.capStructuredRecordString)(args.max_age) : null,
147
+ wake_at: typeof args.wake_at === "string" ? (0, session_events_1.capStructuredRecordString)(args.wake_at) : null,
133
148
  status: "pending",
134
149
  created_at: new Date().toISOString(),
135
150
  filed_from: sessionChannel ?? "unknown",
136
151
  filed_for_friend_id: sessionFriendId ?? null,
152
+ filed_from_key: sessionKey,
153
+ request_id: requestId,
137
154
  };
155
+ let obligationId = null;
156
+ if (requestId && sessionFriendId && sessionChannel && sessionKey) {
157
+ const obligation = (0, obligations_1.createObligation)(agentRoot, {
158
+ origin: { friendId: sessionFriendId, channel: sessionChannel, key: sessionKey },
159
+ owedTo: { friendId: sessionFriendId, channel: sessionChannel, key: sessionKey },
160
+ requestId,
161
+ content: args.condition.trim(),
162
+ });
163
+ (0, obligations_1.advanceObligation)(agentRoot, obligation.id, {
164
+ currentSurface: { kind: "session", label: `${sessionChannel}/${sessionKey}` },
165
+ currentArtifact: `awaiting/${args.name}.md`,
166
+ nextAction: args.condition.trim(),
167
+ });
168
+ obligationId = obligation.id;
169
+ frontmatter.obligation_id = obligation.id;
170
+ }
138
171
  const rendered = (0, await_parser_1.renderAwaitFile)(frontmatter, (0, session_events_1.capStructuredRecordString)(args.body ?? ""));
139
172
  fs.mkdirSync(awaitingDir(agentRoot), { recursive: true });
140
- fs.writeFileSync(filePath, rendered, "utf-8");
173
+ try {
174
+ fs.writeFileSync(filePath, rendered, "utf-8");
175
+ }
176
+ catch (error) {
177
+ if (obligationId)
178
+ (0, obligations_1.fulfillObligation)(agentRoot, obligationId);
179
+ throw error;
180
+ }
141
181
  (0, runtime_1.emitNervesEvent)({
142
182
  component: "repertoire",
143
183
  event: "repertoire.await_filed",
@@ -162,10 +202,14 @@ function archiveAwait(agentRoot, name, updates) {
162
202
  alert: current.alert,
163
203
  mode: current.mode,
164
204
  max_age: current.max_age,
205
+ wake_at: current.wake_at ?? null,
165
206
  status: current.status,
166
207
  created_at: current.created_at,
167
208
  filed_from: current.filed_from,
168
209
  filed_for_friend_id: current.filed_for_friend_id,
210
+ filed_from_key: current.filed_from_key,
211
+ request_id: current.request_id,
212
+ obligation_id: current.obligation_id,
169
213
  ...updates,
170
214
  };
171
215
  const cappedMerged = Object.fromEntries(Object.entries(merged).map(([key, value]) => [
@@ -181,6 +225,10 @@ function archiveAwait(agentRoot, name, updates) {
181
225
  const archived = (0, await_parser_1.parseAwaitFile)(archivedContent, awaitDoneFilePath(agentRoot, name));
182
226
  return { ok: true, file: archived };
183
227
  }
228
+ function fulfillAwaitObligation(agentRoot, awaitFile) {
229
+ if (awaitFile.obligation_id)
230
+ (0, obligations_1.fulfillObligation)(agentRoot, awaitFile.obligation_id);
231
+ }
184
232
  async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName) {
185
233
  const nameError = validateName(name);
186
234
  if (nameError)
@@ -210,7 +258,31 @@ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName
210
258
  });
211
259
  return JSON.stringify({ verdict: "no", recorded: true });
212
260
  }
213
- // verdict === "yes" — archive and alert
261
+ // Request-bound returns stay active through Telegram prepare/send authorization.
262
+ let alert = null;
263
+ if (existing.request_id && existing.filed_from !== "external-event") {
264
+ try {
265
+ alert = await (0, await_alert_1.deliverAwaitAlert)({ awaitFile: existing, reason: "resolved", observation: observation.trim(), agentRoot, agentName, deliveryDeps: resolveDeliveryDeps(agentName) });
266
+ }
267
+ catch (error) {
268
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "repertoire", event: "repertoire.await_alert_error", message: "await alert delivery threw", meta: { agent: agentName, name, error: error instanceof Error ? error.message : String(error) } });
269
+ }
270
+ if (alert?.delivery?.status !== "delivered_now") {
271
+ return JSON.stringify({ verdict: "yes", archived: null, alert: alert ? { attempted: alert.attempted, status: alert.delivery?.status ?? null, skipped: alert.skipped ?? null } : null, advancedExternalEvents: [] });
272
+ }
273
+ }
274
+ const advancedEvents = existing.filed_from === "external-event" && existing.filed_from_key && existing.wake_at
275
+ ? (() => {
276
+ if (!hasActiveExternalEventAwait(agentName, { recordPath: existing.filed_from_key, awaitName: name, wakeAt: existing.wake_at }))
277
+ throw new Error("External event await authority changed before resolution");
278
+ const record = (0, router_1.readExternalEventRecord)(existing.filed_from_key);
279
+ if (record.agent !== agentName || record.recordPath !== existing.filed_from_key || record.executionState !== "handled"
280
+ || record.disposition?.awaitId !== name || record.disposition.nextWake.kind !== "at" || record.disposition.nextWake.at !== existing.wake_at) {
281
+ throw new Error("External event await authority changed before resolution");
282
+ }
283
+ return [(0, router_1.advanceExternalEventFromAwait)(record.recordPath, { awaitId: name, expectedVersion: record.version, expectedGeneration: record.generation })];
284
+ })()
285
+ : [];
214
286
  const archive = archiveAwait(agentRoot, name, {
215
287
  status: "resolved",
216
288
  resolved_at: new Date().toISOString(),
@@ -219,36 +291,26 @@ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName
219
291
  /* v8 ignore next -- defensive: archiveAwait only fails on the file-disappears-mid-call race already covered by v8 ignore inside archiveAwait @preserve */
220
292
  if (!archive.ok)
221
293
  return JSON.stringify({ error: archive.error });
294
+ fulfillAwaitObligation(agentRoot, archive.file);
222
295
  (0, runtime_1.emitNervesEvent)({
223
296
  component: "repertoire",
224
297
  event: "repertoire.await_resolved",
225
298
  message: "await resolved",
226
299
  meta: { agent: agentName, name },
227
300
  });
228
- let alert = null;
229
- try {
230
- alert = await (0, await_alert_1.deliverAwaitAlert)({
231
- awaitFile: archive.file,
232
- reason: "resolved",
233
- observation: observation.trim(),
234
- agentRoot,
235
- agentName,
236
- deliveryDeps: resolveDeliveryDeps(agentName),
237
- });
238
- }
239
- catch (error) {
240
- (0, runtime_1.emitNervesEvent)({
241
- level: "error",
242
- component: "repertoire",
243
- event: "repertoire.await_alert_error",
244
- message: "await alert delivery threw",
245
- meta: { agent: agentName, name, error: error instanceof Error ? error.message : String(error) },
246
- });
301
+ if (!existing.request_id) {
302
+ try {
303
+ alert = await (0, await_alert_1.deliverAwaitAlert)({ awaitFile: archive.file, reason: "resolved", observation: observation.trim(), agentRoot, agentName, deliveryDeps: resolveDeliveryDeps(agentName) });
304
+ }
305
+ catch (error) {
306
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "repertoire", event: "repertoire.await_alert_error", message: "await alert delivery threw", meta: { agent: agentName, name, error: error instanceof Error ? error.message : String(error) } });
307
+ }
247
308
  }
248
309
  return JSON.stringify({
249
310
  verdict: "yes",
250
311
  archived: awaitDoneFilePath(agentRoot, name),
251
312
  alert: alert ? { attempted: alert.attempted, status: alert.delivery?.status ?? null, skipped: alert.skipped ?? null } : null,
313
+ advancedExternalEvents: advancedEvents.map((event) => event.recordPath),
252
314
  });
253
315
  }
254
316
  function cancelAwaitTool(name, reason, agentRoot, agentName) {
@@ -273,6 +335,7 @@ function cancelAwaitTool(name, reason, agentRoot, agentName) {
273
335
  /* v8 ignore next -- defensive: archiveAwait only fails on the file-disappears-mid-call race already covered by v8 ignore inside archiveAwait @preserve */
274
336
  if (!archive.ok)
275
337
  return JSON.stringify({ error: archive.error });
338
+ fulfillAwaitObligation(agentRoot, archive.file);
276
339
  (0, runtime_1.emitNervesEvent)({
277
340
  component: "repertoire",
278
341
  event: "repertoire.await_canceled",
@@ -281,6 +344,105 @@ function cancelAwaitTool(name, reason, agentRoot, agentName) {
281
344
  });
282
345
  return JSON.stringify({ canceled: name, archived: awaitDoneFilePath(agentRoot, name) });
283
346
  }
347
+ function exactFileIsMissing(filePath) {
348
+ try {
349
+ fs.lstatSync(filePath);
350
+ return false;
351
+ }
352
+ catch (error) {
353
+ if (error.code === "ENOENT")
354
+ return true;
355
+ throw error;
356
+ }
357
+ }
358
+ function cancelStaleAwait(agentRoot, agentName, name, reason) {
359
+ const result = JSON.parse(cancelAwaitTool(name, reason, agentRoot, agentName));
360
+ if (result.canceled !== name)
361
+ throw new Error(`Stale await could not be archived for repair: ${result.error ?? name}`);
362
+ }
363
+ function inspectRelationshipFollowUp(agentRoot, input) {
364
+ const awaiting = readAwaitDefinition(agentRoot, input.awaitName);
365
+ if (!awaiting)
366
+ throw new Error(`Await ${input.awaitName} could not be verified`);
367
+ if (!awaiting.obligation_id)
368
+ return { active: false, reason: "request obligation is missing" };
369
+ const obligationPath = path.join(agentRoot, "arc", "obligations", `${awaiting.obligation_id}.json`);
370
+ const obligation = (0, obligations_1.readVerifiedObligations)(agentRoot).find((candidate) => candidate.id === awaiting.obligation_id);
371
+ if (!obligation) {
372
+ if (exactFileIsMissing(obligationPath))
373
+ return { active: false, reason: "request obligation is missing" };
374
+ throw new Error(`Request obligation ${awaiting.obligation_id} exists but could not be verified`);
375
+ }
376
+ if (obligation.status === "fulfilled")
377
+ return { active: false, reason: "request obligation is no longer active" };
378
+ const matches = obligation.requestId === input.requestId
379
+ && obligation.origin.friendId === input.friendId && obligation.origin.channel === input.channel && obligation.origin.key === input.key
380
+ && obligation.owedTo?.friendId === input.friendId && obligation.owedTo.channel === input.channel && obligation.owedTo.key === input.key
381
+ && obligation.currentArtifact === `awaiting/${input.awaitName}.md`
382
+ && awaiting.status === "pending" && awaiting.obligation_id === obligation.id && awaiting.request_id === input.requestId
383
+ && awaiting.filed_for_friend_id === input.friendId && awaiting.filed_from === input.channel && awaiting.filed_from_key === input.key;
384
+ if (!matches)
385
+ return { active: false, reason: "request obligation binding no longer matches" };
386
+ const maxAge = (0, cadence_1.parseCadenceToMs)(awaiting.max_age);
387
+ if (input.allowElapsed !== true && maxAge !== null && awaiting.created_at && (input.now ?? Date.now()) >= Date.parse(awaiting.created_at) + maxAge) {
388
+ return { active: false, reason: "request obligation await expired" };
389
+ }
390
+ return { active: true };
391
+ }
392
+ function hasActiveRelationshipFollowUp(agentRoot, input) {
393
+ if (input.awaitName)
394
+ return inspectRelationshipFollowUp(agentRoot, { ...input, awaitName: input.awaitName }).active;
395
+ const obligation = (0, obligations_1.readVerifiedPendingObligations)(agentRoot).find((candidate) => candidate.requestId === input.requestId
396
+ && candidate.origin.friendId === input.friendId && candidate.origin.channel === input.channel && candidate.origin.key === input.key
397
+ && candidate.owedTo?.friendId === input.friendId && candidate.owedTo.channel === input.channel && candidate.owedTo.key === input.key
398
+ && candidate.currentArtifact?.startsWith("awaiting/") && candidate.currentArtifact.endsWith(".md")
399
+ && (!input.awaitName || candidate.currentArtifact === `awaiting/${input.awaitName}.md`));
400
+ if (!obligation)
401
+ return false;
402
+ const name = path.basename(obligation.currentArtifact, ".md");
403
+ if (!VALID_NAME.test(name) || obligation.currentArtifact !== `awaiting/${name}.md`)
404
+ return false;
405
+ const awaiting = readAwaitDefinition(agentRoot, name);
406
+ if (!awaiting || awaiting.status !== "pending" || awaiting.obligation_id !== obligation.id || awaiting.request_id !== input.requestId
407
+ || awaiting.filed_for_friend_id !== input.friendId || awaiting.filed_from !== input.channel || awaiting.filed_from_key !== input.key)
408
+ return false;
409
+ const maxAge = (0, cadence_1.parseCadenceToMs)(awaiting.max_age);
410
+ return input.allowElapsed === true || maxAge === null || !awaiting.created_at || (input.now ?? Date.now()) < Date.parse(awaiting.created_at) + maxAge;
411
+ }
412
+ function hasActiveExternalEventAwait(agentName, input, root = (0, router_1.getExternalEventRoot)()) {
413
+ return inspectExternalEventAwait(agentName, input, root).active;
414
+ }
415
+ function inspectExternalEventAwait(agentName, input, root = (0, router_1.getExternalEventRoot)()) {
416
+ const relative = path.relative(path.resolve(root), path.resolve(input.recordPath));
417
+ if (relative.startsWith("..") || path.isAbsolute(relative))
418
+ throw new Error("External event await record is outside the event root");
419
+ let record;
420
+ try {
421
+ record = (0, router_1.readExternalEventRecord)(input.recordPath);
422
+ }
423
+ catch (error) {
424
+ if (error.code === "ENOENT")
425
+ return { active: false, reason: "external event disposition is missing" };
426
+ throw error;
427
+ }
428
+ const active = record.agent === agentName && record.recordPath === input.recordPath && record.executionState === "handled"
429
+ && record.disposition?.awaitId === input.awaitName && record.disposition.nextWake.kind === "at" && record.disposition.nextWake.at === input.wakeAt;
430
+ return active ? { active: true } : { active: false, reason: "external event disposition is no longer active" };
431
+ }
432
+ function cancelRelationshipFollowUps(agentRoot, agentName, input) {
433
+ const obligations = (0, obligations_1.readVerifiedPendingObligations)(agentRoot).filter((candidate) => candidate.owedTo?.friendId === input.friendId
434
+ && candidate.owedTo.channel === input.channel && candidate.owedTo.key === input.key);
435
+ for (const obligation of obligations) {
436
+ const artifact = obligation.currentArtifact;
437
+ const name = artifact?.startsWith("awaiting/") && artifact.endsWith(".md") ? path.basename(artifact, ".md") : null;
438
+ if (name && VALID_NAME.test(name) && artifact === `awaiting/${name}.md` && readAwaitDefinition(agentRoot, name)) {
439
+ cancelAwaitTool(name, "relationship revoked", agentRoot, agentName);
440
+ }
441
+ else {
442
+ (0, obligations_1.fulfillObligation)(agentRoot, obligation.id);
443
+ }
444
+ }
445
+ }
284
446
  exports.awaitingToolDefinitions = [
285
447
  {
286
448
  tool: {
@@ -297,6 +459,7 @@ exports.awaitingToolDefinitions = [
297
459
  alert: { type: "string", description: "Channel to alert on (e.g. 'bluebubbles', 'teams'). Defaults to filing session's channel." },
298
460
  mode: { type: "string", description: "'full' or 'quick'. Defaults 'full'." },
299
461
  max_age: { type: "string", description: "Optional auto-expiry (e.g. '24h')." },
462
+ wake_at: { type: "string", description: "Optional exact canonical ISO time this Await supports." },
300
463
  body: { type: "string", description: "Optional notes: why I filed this, what 'ready' looks like." },
301
464
  },
302
465
  required: ["name", "condition", "cadence"],
@@ -306,6 +469,10 @@ exports.awaitingToolDefinitions = [
306
469
  handler: (a, ctx) => {
307
470
  const agentRoot = (0, identity_1.getAgentRoot)();
308
471
  const agentName = (0, identity_1.getAgentName)();
472
+ const event = ctx?.currentExternalEvent;
473
+ const eventFriendId = event ? ctx?.context?.friend.id ?? null : null;
474
+ if (event && !eventFriendId)
475
+ return JSON.stringify({ error: "external event await authority has no exact owner relationship" });
309
476
  return fileAwait({
310
477
  name: a.name,
311
478
  condition: a.condition,
@@ -313,8 +480,9 @@ exports.awaitingToolDefinitions = [
313
480
  alert: a.alert,
314
481
  mode: a.mode,
315
482
  max_age: a.max_age,
483
+ wake_at: a.wake_at,
316
484
  body: a.body,
317
- }, agentRoot, agentName, ctx?.currentSession?.friendId ?? null, ctx?.currentSession?.channel ?? null);
485
+ }, agentRoot, agentName, eventFriendId ?? ctx?.currentSession?.friendId ?? null, event ? "external-event" : ctx?.currentSession?.channel ?? null, event ? event.recordPath : ctx?.currentSession?.key ?? null, event ? null : ctx?.relationshipAuthorization?.requestId ?? null);
318
486
  },
319
487
  riskProfile: { mutates: "durable_state_write", risk: "high", reason: "files a durable await condition" },
320
488
  },
@@ -335,9 +503,32 @@ exports.awaitingToolDefinitions = [
335
503
  },
336
504
  },
337
505
  },
338
- handler: async (a) => {
506
+ handler: async (a, ctx) => {
339
507
  const agentRoot = (0, identity_1.getAgentRoot)();
340
508
  const agentName = (0, identity_1.getAgentName)();
509
+ if (ctx?.relationshipAuthorization) {
510
+ const session = ctx.currentSession;
511
+ const requestId = ctx.relationshipAuthorization.requestId;
512
+ const existing = readAwaitDefinition(agentRoot, String(a.name ?? ""));
513
+ const awaitName = String(a.name ?? "");
514
+ let binding = null;
515
+ if (session?.channel === "external-event" && existing?.wake_at) {
516
+ binding = inspectExternalEventAwait(agentName, { recordPath: session.key, awaitName, wakeAt: existing.wake_at });
517
+ }
518
+ else if (session && requestId) {
519
+ if (existing?.filed_for_friend_id !== session.friendId || existing.filed_from !== session.channel
520
+ || existing.filed_from_key !== session.key || existing.request_id !== requestId) {
521
+ return JSON.stringify({ error: "resolve_await is limited to the current relationship request or bound external event" });
522
+ }
523
+ binding = inspectRelationshipFollowUp(agentRoot, { friendId: session.friendId, channel: session.channel, key: session.key, requestId, awaitName });
524
+ }
525
+ if (binding && !binding.active) {
526
+ cancelStaleAwait(agentRoot, agentName, awaitName, binding.reason);
527
+ return JSON.stringify({ error: binding.reason });
528
+ }
529
+ if (!binding?.active)
530
+ return JSON.stringify({ error: "resolve_await is limited to the current relationship request or bound external event" });
531
+ }
341
532
  return resolveAwaitTool(a.name, a.verdict, a.observation, agentRoot, agentName);
342
533
  },
343
534
  riskProfile: {
@@ -362,9 +553,21 @@ exports.awaitingToolDefinitions = [
362
553
  },
363
554
  },
364
555
  },
365
- handler: (a) => {
556
+ handler: (a, ctx) => {
366
557
  const agentRoot = (0, identity_1.getAgentRoot)();
367
558
  const agentName = (0, identity_1.getAgentName)();
559
+ if (ctx?.relationshipAuthorization) {
560
+ const session = ctx.currentSession;
561
+ const requestId = ctx.relationshipAuthorization.requestId;
562
+ if (!session || !requestId || !hasActiveRelationshipFollowUp(agentRoot, {
563
+ friendId: session.friendId,
564
+ channel: session.channel,
565
+ key: session.key,
566
+ requestId,
567
+ awaitName: String(a.name ?? ""),
568
+ }))
569
+ return JSON.stringify({ error: "cancel_await is limited to the current relationship request" });
570
+ }
368
571
  return cancelAwaitTool(a.name, a.reason, agentRoot, agentName);
369
572
  },
370
573
  riskProfile: { mutates: "durable_state_write", risk: "high", reason: "archives a durable await condition" },
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = exports.renderInnerProgressStatus = exports.speakTool = exports.restTool = exports.settleTool = exports.observeTool = exports.ponderTool = void 0;
4
+ exports.routineActionRequester = routineActionRequester;
4
5
  const tools_files_1 = require("./tools-files");
5
6
  const tools_shell_1 = require("./tools-shell");
6
7
  const tools_notes_1 = require("./tools-notes");
@@ -28,6 +29,7 @@ const tools_runtime_1 = require("./tools-runtime");
28
29
  const tools_orientation_1 = require("./tools-orientation");
29
30
  const tools_rsvp_1 = require("./tools-rsvp");
30
31
  const tools_habits_1 = require("./tools-habits");
32
+ const tools_telegram_contacts_1 = require("./tools-telegram-contacts");
31
33
  // Re-export flow tools for consumers that import them from tools-base
32
34
  var tools_flow_1 = require("./tools-flow");
33
35
  Object.defineProperty(exports, "ponderTool", { enumerable: true, get: function () { return tools_flow_1.ponderTool; } });
@@ -38,6 +40,18 @@ Object.defineProperty(exports, "speakTool", { enumerable: true, get: function ()
38
40
  // Re-export renderInnerProgressStatus for consumers
39
41
  var tools_session_2 = require("./tools-session");
40
42
  Object.defineProperty(exports, "renderInnerProgressStatus", { enumerable: true, get: function () { return tools_session_2.renderInnerProgressStatus; } });
43
+ function routineActionRequester(ctx) {
44
+ const actor = ctx?.relationshipAuthorization?.actor;
45
+ if (!actor)
46
+ return null;
47
+ if (actor.trustLevel === "family")
48
+ return { kind: "owner", friendId: actor.friendId };
49
+ const requestId = ctx?.relationshipAuthorization?.requestId;
50
+ const session = ctx?.currentSession;
51
+ if (actor.trustLevel !== "friend" || !requestId || !session || session.friendId !== actor.friendId)
52
+ return null;
53
+ return { kind: "household_request", friendId: actor.friendId, requestId, origin: { friendId: session.friendId, channel: session.channel, key: session.key } };
54
+ }
41
55
  // Tracks which file paths have been read via read_file in this session.
42
56
  // edit_file requires a file to be read first (must-read-first guard).
43
57
  exports.editFileReadTracker = new Set();
@@ -71,6 +85,7 @@ exports.baseToolDefinitions = [
71
85
  ...tools_runtime_1.runtimeToolDefinitions,
72
86
  ...tools_rsvp_1.rsvpToolDefinitions,
73
87
  ...tools_habits_1.habitToolDefinitions,
88
+ tools_telegram_contacts_1.telegramContactToolDefinition,
74
89
  ];
75
90
  // Convenience array of just the tool schemas (no handler/integration metadata).
76
91
  // Used by consumers that need the OpenAI function-tool format.