@ouro.bot/cli 0.1.0-alpha.595 → 0.1.0-alpha.597

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.
@@ -0,0 +1,360 @@
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.awaitingToolDefinitions = void 0;
37
+ exports.setAwaitToolDeps = setAwaitToolDeps;
38
+ exports.resetAwaitToolDeps = resetAwaitToolDeps;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const identity_1 = require("../heart/identity");
42
+ const runtime_1 = require("../nerves/runtime");
43
+ const await_parser_1 = require("../heart/awaiting/await-parser");
44
+ const await_alert_1 = require("../heart/awaiting/await-alert");
45
+ const pending_1 = require("../mind/pending");
46
+ /**
47
+ * Bundle-root-relative locations.
48
+ * - `awaiting/<name>.md` — active awaits (status: pending)
49
+ * - `awaiting/.done/<name>.md` — terminal awaits (resolved/expired/canceled)
50
+ */
51
+ function awaitingDir(agentRoot) {
52
+ return path.join(agentRoot, "awaiting");
53
+ }
54
+ function awaitingDoneDir(agentRoot) {
55
+ return path.join(awaitingDir(agentRoot), ".done");
56
+ }
57
+ function awaitFilePath(agentRoot, name) {
58
+ return path.join(awaitingDir(agentRoot), `${name}.md`);
59
+ }
60
+ function awaitDoneFilePath(agentRoot, name) {
61
+ return path.join(awaitingDoneDir(agentRoot), `${name}.md`);
62
+ }
63
+ const VALID_NAME = /^[A-Za-z0-9_-]+$/;
64
+ function validateName(name) {
65
+ if (!name)
66
+ return "name is required";
67
+ if (!VALID_NAME.test(name))
68
+ return "name must be alphanumeric, underscores, or hyphens";
69
+ return null;
70
+ }
71
+ function readAwaitDefinition(agentRoot, name) {
72
+ const filePath = awaitFilePath(agentRoot, name);
73
+ try {
74
+ const content = fs.readFileSync(filePath, "utf-8");
75
+ return (0, await_parser_1.parseAwaitFile)(content, filePath);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Default delivery deps for the await alert path used from the tool.
83
+ * Mirrors the proactive-outreach pattern: queue to the inner-dialog pending
84
+ * dir when no live deliverer is registered.
85
+ */
86
+ function defaultDeliveryDeps(agentName) {
87
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)(agentName);
88
+ return {
89
+ agentName,
90
+ queuePending: (message) => {
91
+ // Mirror the write-as-pending convention from tools-session.
92
+ fs.mkdirSync(pendingDir, { recursive: true });
93
+ const filename = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
94
+ fs.writeFileSync(path.join(pendingDir, filename), JSON.stringify(message, null, 2), "utf-8");
95
+ },
96
+ };
97
+ }
98
+ let injected = {};
99
+ function setAwaitToolDeps(deps) {
100
+ injected = deps;
101
+ }
102
+ function resetAwaitToolDeps() {
103
+ injected = {};
104
+ }
105
+ function resolveDeliveryDeps(agentName) {
106
+ if (injected.buildDeliveryDeps)
107
+ return injected.buildDeliveryDeps(agentName);
108
+ return defaultDeliveryDeps(agentName);
109
+ }
110
+ function fileAwait(args, agentRoot, agentName, sessionFriendId, sessionChannel) {
111
+ const nameError = validateName(args.name);
112
+ if (nameError)
113
+ return JSON.stringify({ error: nameError });
114
+ if (!args.condition || !args.condition.trim()) {
115
+ return JSON.stringify({ error: "condition is required" });
116
+ }
117
+ if (!args.cadence || !args.cadence.trim()) {
118
+ return JSON.stringify({ error: "cadence is required" });
119
+ }
120
+ const filePath = awaitFilePath(agentRoot, args.name);
121
+ if (fs.existsSync(filePath)) {
122
+ return JSON.stringify({ error: `await "${args.name}" already exists` });
123
+ }
124
+ const mode = args.mode === "quick" ? "quick" : "full";
125
+ const alert = args.alert ?? sessionChannel ?? null;
126
+ const frontmatter = {
127
+ condition: args.condition.trim(),
128
+ cadence: args.cadence.trim(),
129
+ alert,
130
+ mode,
131
+ max_age: args.max_age ?? null,
132
+ status: "pending",
133
+ created_at: new Date().toISOString(),
134
+ filed_from: sessionChannel ?? "unknown",
135
+ filed_for_friend_id: sessionFriendId ?? null,
136
+ };
137
+ const rendered = (0, await_parser_1.renderAwaitFile)(frontmatter, args.body ?? "");
138
+ fs.mkdirSync(awaitingDir(agentRoot), { recursive: true });
139
+ fs.writeFileSync(filePath, rendered, "utf-8");
140
+ (0, runtime_1.emitNervesEvent)({
141
+ component: "repertoire",
142
+ event: "repertoire.await_filed",
143
+ message: "filed new await",
144
+ meta: { agent: agentName, name: args.name, cadence: args.cadence, alert },
145
+ });
146
+ return JSON.stringify({ filed: args.name, path: filePath });
147
+ }
148
+ function archiveAwait(agentRoot, name, updates) {
149
+ const source = awaitFilePath(agentRoot, name);
150
+ /* v8 ignore start -- defensive: callers (resolve/cancel) already verify the file exists via readAwaitDefinition; this guards the file-disappears-between-calls race @preserve */
151
+ if (!fs.existsSync(source)) {
152
+ return { ok: false, error: `await "${name}" not found in awaiting/` };
153
+ }
154
+ /* v8 ignore stop */
155
+ const content = fs.readFileSync(source, "utf-8");
156
+ const current = (0, await_parser_1.parseAwaitFile)(content, source);
157
+ // merge frontmatter from the parsed file with updates
158
+ const merged = {
159
+ condition: current.condition,
160
+ cadence: current.cadence,
161
+ alert: current.alert,
162
+ mode: current.mode,
163
+ max_age: current.max_age,
164
+ status: current.status,
165
+ created_at: current.created_at,
166
+ filed_from: current.filed_from,
167
+ filed_for_friend_id: current.filed_for_friend_id,
168
+ ...updates,
169
+ };
170
+ const rendered = (0, await_parser_1.renderAwaitFile)(merged, current.body);
171
+ fs.mkdirSync(awaitingDoneDir(agentRoot), { recursive: true });
172
+ fs.writeFileSync(awaitDoneFilePath(agentRoot, name), rendered, "utf-8");
173
+ fs.unlinkSync(source);
174
+ // re-parse the archived file so callers see merged fields (e.g. resolution_observation)
175
+ const archivedContent = fs.readFileSync(awaitDoneFilePath(agentRoot, name), "utf-8");
176
+ const archived = (0, await_parser_1.parseAwaitFile)(archivedContent, awaitDoneFilePath(agentRoot, name));
177
+ return { ok: true, file: archived };
178
+ }
179
+ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName) {
180
+ const nameError = validateName(name);
181
+ if (nameError)
182
+ return JSON.stringify({ error: nameError });
183
+ const existing = readAwaitDefinition(agentRoot, name);
184
+ if (!existing) {
185
+ return JSON.stringify({ error: `await "${name}" not found in awaiting/` });
186
+ }
187
+ if (existing.status !== "pending") {
188
+ return JSON.stringify({ error: `await "${name}" is not pending (status: ${existing.status})` });
189
+ }
190
+ if (verdict !== "yes" && verdict !== "no") {
191
+ return JSON.stringify({ error: "verdict must be 'yes' or 'no'" });
192
+ }
193
+ if (!observation || !observation.trim()) {
194
+ return JSON.stringify({ error: "observation is required" });
195
+ }
196
+ if (verdict === "no") {
197
+ // Update runtime state via recordAwaitCheck-style write
198
+ const { recordAwaitCheck } = await Promise.resolve().then(() => __importStar(require("../heart/awaiting/await-runtime-state")));
199
+ recordAwaitCheck(agentRoot, name, observation.trim(), new Date().toISOString());
200
+ (0, runtime_1.emitNervesEvent)({
201
+ component: "repertoire",
202
+ event: "repertoire.await_check_no",
203
+ message: "await checked, not yet ready",
204
+ meta: { agent: agentName, name },
205
+ });
206
+ return JSON.stringify({ verdict: "no", recorded: true });
207
+ }
208
+ // verdict === "yes" — archive and alert
209
+ const archive = archiveAwait(agentRoot, name, {
210
+ status: "resolved",
211
+ resolved_at: new Date().toISOString(),
212
+ resolution_observation: observation.trim(),
213
+ });
214
+ /* v8 ignore next -- defensive: archiveAwait only fails on the file-disappears-mid-call race already covered by v8 ignore inside archiveAwait @preserve */
215
+ if (!archive.ok)
216
+ return JSON.stringify({ error: archive.error });
217
+ (0, runtime_1.emitNervesEvent)({
218
+ component: "repertoire",
219
+ event: "repertoire.await_resolved",
220
+ message: "await resolved",
221
+ meta: { agent: agentName, name },
222
+ });
223
+ let alert = null;
224
+ try {
225
+ alert = await (0, await_alert_1.deliverAwaitAlert)({
226
+ awaitFile: archive.file,
227
+ reason: "resolved",
228
+ observation: observation.trim(),
229
+ agentRoot,
230
+ agentName,
231
+ deliveryDeps: resolveDeliveryDeps(agentName),
232
+ });
233
+ }
234
+ catch (error) {
235
+ (0, runtime_1.emitNervesEvent)({
236
+ level: "error",
237
+ component: "repertoire",
238
+ event: "repertoire.await_alert_error",
239
+ message: "await alert delivery threw",
240
+ meta: { agent: agentName, name, error: error instanceof Error ? error.message : String(error) },
241
+ });
242
+ }
243
+ return JSON.stringify({
244
+ verdict: "yes",
245
+ archived: awaitDoneFilePath(agentRoot, name),
246
+ alert: alert ? { attempted: alert.attempted, status: alert.delivery?.status ?? null, skipped: alert.skipped ?? null } : null,
247
+ });
248
+ }
249
+ function cancelAwaitTool(name, reason, agentRoot, agentName) {
250
+ const nameError = validateName(name);
251
+ if (nameError)
252
+ return JSON.stringify({ error: nameError });
253
+ const existing = readAwaitDefinition(agentRoot, name);
254
+ if (!existing) {
255
+ return JSON.stringify({ error: `await "${name}" not found in awaiting/` });
256
+ }
257
+ if (existing.status !== "pending") {
258
+ return JSON.stringify({ error: `await "${name}" is not pending (status: ${existing.status})` });
259
+ }
260
+ const updates = {
261
+ status: "canceled",
262
+ canceled_at: new Date().toISOString(),
263
+ };
264
+ if (reason && reason.trim()) {
265
+ updates.cancel_reason = reason.trim();
266
+ }
267
+ const archive = archiveAwait(agentRoot, name, updates);
268
+ /* v8 ignore next -- defensive: archiveAwait only fails on the file-disappears-mid-call race already covered by v8 ignore inside archiveAwait @preserve */
269
+ if (!archive.ok)
270
+ return JSON.stringify({ error: archive.error });
271
+ (0, runtime_1.emitNervesEvent)({
272
+ component: "repertoire",
273
+ event: "repertoire.await_canceled",
274
+ message: "await canceled",
275
+ meta: { agent: agentName, name },
276
+ });
277
+ return JSON.stringify({ canceled: name, archived: awaitDoneFilePath(agentRoot, name) });
278
+ }
279
+ exports.awaitingToolDefinitions = [
280
+ {
281
+ tool: {
282
+ type: "function",
283
+ function: {
284
+ name: "await_condition",
285
+ description: "File a one-shot waiting condition. The daemon polls on cadence; on each tick I evaluate the condition and call resolve_await. When the condition becomes true, an alert fires via my outward channel.",
286
+ parameters: {
287
+ type: "object",
288
+ properties: {
289
+ name: { type: "string", description: "Filename stem (alphanumeric/underscore/hyphen). Must be unique." },
290
+ condition: { type: "string", description: "Natural-language condition to watch for." },
291
+ cadence: { type: "string", description: "Polling cadence (e.g. '5m', '1h')." },
292
+ alert: { type: "string", description: "Channel to alert on (e.g. 'bluebubbles', 'teams'). Defaults to filing session's channel." },
293
+ mode: { type: "string", description: "'full' or 'quick'. Defaults 'full'." },
294
+ max_age: { type: "string", description: "Optional auto-expiry (e.g. '24h')." },
295
+ body: { type: "string", description: "Optional notes: why I filed this, what 'ready' looks like." },
296
+ },
297
+ required: ["name", "condition", "cadence"],
298
+ },
299
+ },
300
+ },
301
+ handler: (a, ctx) => {
302
+ const agentRoot = (0, identity_1.getAgentRoot)();
303
+ const agentName = (0, identity_1.getAgentName)();
304
+ return fileAwait({
305
+ name: a.name,
306
+ condition: a.condition,
307
+ cadence: a.cadence,
308
+ alert: a.alert,
309
+ mode: a.mode,
310
+ max_age: a.max_age,
311
+ body: a.body,
312
+ }, agentRoot, agentName, ctx?.currentSession?.friendId ?? null, ctx?.currentSession?.channel ?? null);
313
+ },
314
+ },
315
+ {
316
+ tool: {
317
+ type: "function",
318
+ function: {
319
+ name: "resolve_await",
320
+ description: "Resolve a pending await with a verdict. verdict='yes' archives and fires the alert. verdict='no' records the observation and continues polling.",
321
+ parameters: {
322
+ type: "object",
323
+ properties: {
324
+ name: { type: "string", description: "Await name (filename stem)." },
325
+ verdict: { type: "string", description: "'yes' if the condition is met, 'no' otherwise." },
326
+ observation: { type: "string", description: "One-line summary of what I saw this tick." },
327
+ },
328
+ required: ["name", "verdict", "observation"],
329
+ },
330
+ },
331
+ },
332
+ handler: async (a) => {
333
+ const agentRoot = (0, identity_1.getAgentRoot)();
334
+ const agentName = (0, identity_1.getAgentName)();
335
+ return resolveAwaitTool(a.name, a.verdict, a.observation, agentRoot, agentName);
336
+ },
337
+ },
338
+ {
339
+ tool: {
340
+ type: "function",
341
+ function: {
342
+ name: "cancel_await",
343
+ description: "Cancel a pending await without alerting. Archives with status: canceled.",
344
+ parameters: {
345
+ type: "object",
346
+ properties: {
347
+ name: { type: "string", description: "Await name (filename stem)." },
348
+ reason: { type: "string", description: "Optional cancel reason." },
349
+ },
350
+ required: ["name"],
351
+ },
352
+ },
353
+ },
354
+ handler: (a) => {
355
+ const agentRoot = (0, identity_1.getAgentRoot)();
356
+ const agentName = (0, identity_1.getAgentName)();
357
+ return cancelAwaitTool(a.name, a.reason, agentRoot, agentName);
358
+ },
359
+ },
360
+ ];
@@ -18,6 +18,8 @@ const tools_flight_1 = require("./tools-flight");
18
18
  const tools_attachments_1 = require("./tools-attachments");
19
19
  const tools_mail_1 = require("./tools-mail");
20
20
  const tools_trip_1 = require("./tools-trip");
21
+ const tools_awaiting_1 = require("./tools-awaiting");
22
+ const tools_obligations_1 = require("./tools-obligations");
21
23
  // Re-export flow tools for consumers that import them from tools-base
22
24
  var tools_flow_1 = require("./tools-flow");
23
25
  Object.defineProperty(exports, "ponderTool", { enumerable: true, get: function () { return tools_flow_1.ponderTool; } });
@@ -51,6 +53,8 @@ exports.baseToolDefinitions = [
51
53
  ...tools_attachments_1.attachmentToolDefinitions,
52
54
  ...tools_mail_1.mailToolDefinitions,
53
55
  ...tools_trip_1.tripToolDefinitions,
56
+ ...tools_awaiting_1.awaitingToolDefinitions,
57
+ ...tools_obligations_1.obligationToolDefinitions,
54
58
  ];
55
59
  // Convenience array of just the tool schemas (no handler/integration metadata).
56
60
  // Used by consumers that need the OpenAI function-tool format.
@@ -0,0 +1,142 @@
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.obligationToolDefinitions = void 0;
37
+ const path = __importStar(require("path"));
38
+ const identity_1 = require("../heart/identity");
39
+ const runtime_1 = require("../nerves/runtime");
40
+ const json_store_1 = require("../arc/json-store");
41
+ const obligations_1 = require("../arc/obligations");
42
+ /**
43
+ * `let_go` lets the agent release a held work item from its attention loop.
44
+ *
45
+ * Two stores need release semantics:
46
+ * 1. ReturnObligations at `arc/obligations/inner/<id>.json` (status: queued|running|returned|deferred)
47
+ * — these are what surface in the "held work items" section of the prompt.
48
+ * 2. Outer Obligations at `arc/obligations/<id>.json` (status: pending|...|fulfilled)
49
+ * — these surface as "i owe <friend>: ..." in the commitments section.
50
+ *
51
+ * The pre-existing path to terminal state for both is the `surface` tool, which fulfills
52
+ * an obligation as a side-effect of delivering a response. When work is resolved EXTERNALLY
53
+ * (e.g. a PR merged fixed the underlying issue), there is no response to surface — the
54
+ * agent has no way to clear the item, so it cycles in the prompt every turn.
55
+ *
56
+ * `let_go` is the missing primitive: dismissal WITHOUT delivery.
57
+ */
58
+ function outerObligationsDir(agentRoot) {
59
+ return path.join(agentRoot, "arc", "obligations");
60
+ }
61
+ function readOuterObligation(agentRoot, id) {
62
+ return (0, json_store_1.readJsonFile)(outerObligationsDir(agentRoot), id) ?? null;
63
+ }
64
+ function letGo(args, agentRoot, agentName) {
65
+ if (typeof args.id !== "string" || args.id.trim().length === 0) {
66
+ return JSON.stringify({ error: "id is required" });
67
+ }
68
+ const id = args.id.trim();
69
+ const reason = typeof args.reason === "string" && args.reason.trim().length > 0
70
+ ? args.reason.trim()
71
+ : null;
72
+ // 1. ReturnObligation (inner) first — these are what slugger sees in "held work items".
73
+ const ret = (0, obligations_1.readReturnObligation)(agentName, id);
74
+ if (ret) {
75
+ if (ret.status === "returned" || ret.status === "deferred") {
76
+ return JSON.stringify({ kind: "return_obligation", id, already: ret.status });
77
+ }
78
+ (0, obligations_1.advanceReturnObligation)(agentName, id, {
79
+ status: "returned",
80
+ returnedAt: Date.now(),
81
+ returnTarget: "surface",
82
+ });
83
+ (0, runtime_1.emitNervesEvent)({
84
+ component: "repertoire",
85
+ event: "repertoire.obligation_let_go",
86
+ message: "agent let go of return obligation",
87
+ meta: { kind: "return_obligation", id, reason: reason ?? null },
88
+ });
89
+ return JSON.stringify({ kind: "return_obligation", let_go: id, reason });
90
+ }
91
+ // 2. Outer Obligation — these surface as "i owe ..." in commitments.
92
+ const outer = readOuterObligation(agentRoot, id);
93
+ if (outer) {
94
+ if (outer.status === "fulfilled") {
95
+ return JSON.stringify({ kind: "obligation", id, already: "fulfilled" });
96
+ }
97
+ (0, obligations_1.fulfillObligation)(agentRoot, id);
98
+ if (reason !== null) {
99
+ // Persist the dismissal reason as the obligation's latestNote so future-me
100
+ // can read why this was released (the nerves event also captures it).
101
+ (0, obligations_1.advanceObligation)(agentRoot, id, { latestNote: reason });
102
+ }
103
+ (0, runtime_1.emitNervesEvent)({
104
+ component: "repertoire",
105
+ event: "repertoire.obligation_let_go",
106
+ message: "agent let go of outer obligation",
107
+ meta: { kind: "obligation", id, reason: reason ?? null },
108
+ });
109
+ return JSON.stringify({ kind: "obligation", let_go: id, reason });
110
+ }
111
+ return JSON.stringify({ error: `no obligation found with id "${id}"` });
112
+ }
113
+ exports.obligationToolDefinitions = [
114
+ {
115
+ tool: {
116
+ type: "function",
117
+ function: {
118
+ name: "let_go",
119
+ description: "release a held work item from my attention. for items that have been resolved externally, no longer apply, or are stale and just cycling in my prompt with nothing to act on. takes the id i see in the 'held work items' section (or any obligation id from arc/obligations/). optional reason is recorded for future me. idempotent — calling on an already-released item returns the existing status, not an error.",
120
+ parameters: {
121
+ type: "object",
122
+ properties: {
123
+ id: {
124
+ type: "string",
125
+ description: "the obligation id — the bracketed id in 'held work items' (e.g. '1775976317954-s5pno43r'), or any obligation file's id from arc/obligations/.",
126
+ },
127
+ reason: {
128
+ type: "string",
129
+ description: "optional one-line reason i'm letting go (e.g. 'resolved externally by PR #701').",
130
+ },
131
+ },
132
+ required: ["id"],
133
+ },
134
+ },
135
+ },
136
+ handler: (args) => {
137
+ const agentRoot = (0, identity_1.getAgentRoot)();
138
+ const agentName = (0, identity_1.getAgentName)();
139
+ return letGo({ id: args.id, reason: args.reason }, agentRoot, agentName);
140
+ },
141
+ },
142
+ ];
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAwaitTurnMessage = buildAwaitTurnMessage;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ function formatElapsed(ms) {
6
+ if (ms < 60_000)
7
+ return "<1m ago";
8
+ const minutes = Math.floor(ms / 60_000);
9
+ if (minutes < 60)
10
+ return `${minutes}m ago`;
11
+ const hours = Math.floor(minutes / 60);
12
+ if (hours < 24)
13
+ return `${hours}h ago`;
14
+ const days = Math.floor(hours / 24);
15
+ return `${days}d ago`;
16
+ }
17
+ function relativeAge(lastCheckedAt, now) {
18
+ if (!lastCheckedAt)
19
+ return null;
20
+ const lastMs = new Date(lastCheckedAt).getTime();
21
+ if (!Number.isFinite(lastMs))
22
+ return null;
23
+ return formatElapsed(now().getTime() - lastMs);
24
+ }
25
+ function buildAwaitTurnMessage(options) {
26
+ (0, runtime_1.emitNervesEvent)({
27
+ component: "senses",
28
+ event: "senses.await_turn_message_built",
29
+ message: "built await tick message",
30
+ meta: { awaitName: options.awaitName, checkedCount: options.checkedCount },
31
+ });
32
+ const lines = [];
33
+ lines.push(`await tick: ${options.awaitName} — ${options.condition}`);
34
+ if (options.body && options.body.trim().length > 0) {
35
+ lines.push("");
36
+ lines.push("what would count as ready:");
37
+ lines.push(options.body.trim());
38
+ }
39
+ const age = relativeAge(options.lastCheckedAt, options.now);
40
+ const obs = options.lastObservation && options.lastObservation.trim().length > 0
41
+ ? `last observation: "${options.lastObservation.trim()}"`
42
+ : "last observation: (none yet)";
43
+ if (options.checkedCount === 0) {
44
+ lines.push("");
45
+ lines.push("history: never checked. this is my first look.");
46
+ }
47
+ else {
48
+ lines.push("");
49
+ lines.push(`history: checked ${options.checkedCount}x so far. last checked ${age ?? "(unknown)"}. ${obs}.`);
50
+ }
51
+ if (options.checkpoint) {
52
+ lines.push("");
53
+ lines.push(`last checkpoint: ${options.checkpoint}`);
54
+ }
55
+ lines.push("");
56
+ lines.push("look around and decide. if the condition is met, call resolve_await with verdict='yes' and a one-line observation. otherwise call resolve_await with verdict='no' and a one-line observation of what i saw this tick.");
57
+ return lines.join("\n");
58
+ }
@@ -119,9 +119,9 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
119
119
  });
120
120
  }
121
121
  }
122
- async function run(reason, taskId, habitName) {
122
+ async function run(reason, taskId, habitName, awaitName) {
123
123
  if (running) {
124
- queue.push({ reason, taskId, habitName });
124
+ queue.push({ reason, taskId, habitName, awaitName });
125
125
  return;
126
126
  }
127
127
  running = true;
@@ -129,10 +129,11 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
129
129
  let nextReason = reason;
130
130
  let nextTaskId = taskId;
131
131
  let nextHabitName = habitName;
132
+ let nextAwaitName = awaitName;
132
133
  let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
133
134
  do {
134
135
  try {
135
- await runTurn({ reason: nextReason, taskId: nextTaskId, habitName: nextHabitName });
136
+ await runTurn({ reason: nextReason, taskId: nextTaskId, habitName: nextHabitName, awaitName: nextAwaitName });
136
137
  }
137
138
  catch (error) {
138
139
  (0, runtime_1.emitNervesEvent)({
@@ -165,6 +166,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
165
166
  nextReason = next.reason;
166
167
  nextTaskId = next.taskId;
167
168
  nextHabitName = next.habitName;
169
+ nextAwaitName = next.awaitName;
168
170
  consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
169
171
  continue;
170
172
  }
@@ -191,6 +193,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
191
193
  nextReason = "instinct";
192
194
  nextTaskId = undefined;
193
195
  nextHabitName = undefined;
196
+ nextAwaitName = undefined;
194
197
  continue;
195
198
  }
196
199
  break;
@@ -211,6 +214,13 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
211
214
  await run("habit", undefined, maybeMessage.habitName);
212
215
  return;
213
216
  }
217
+ if (maybeMessage.type === "await") {
218
+ /* v8 ignore next -- defensive fallback: live await dispatch always sets awaitName @preserve */
219
+ const awaitName = maybeMessage.awaitName ?? "(unnamed)";
220
+ recordHabitFireForRecursion(`await:${awaitName}`);
221
+ await run("await", undefined, undefined, maybeMessage.awaitName);
222
+ return;
223
+ }
214
224
  if (maybeMessage.type === "heartbeat") {
215
225
  // Backward compatibility: heartbeat -> habit/heartbeat
216
226
  recordHabitFireForRecursion("heartbeat");