@ouro.bot/cli 0.1.0-alpha.665 → 0.1.0-alpha.667

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 (35) hide show
  1. package/changelog.json +13 -0
  2. package/dist/arc/flight-recorder.js +324 -5
  3. package/dist/heart/core.js +167 -4
  4. package/dist/heart/cross-chat-delivery.js +3 -2
  5. package/dist/heart/daemon/cli-exec.js +139 -1
  6. package/dist/heart/daemon/cli-help.js +13 -2
  7. package/dist/heart/daemon/cli-parse.js +138 -2
  8. package/dist/heart/daemon/daemon-entry.js +24 -5
  9. package/dist/heart/daemon/daemon.js +10 -1
  10. package/dist/heart/habits/habit-parser.js +8 -0
  11. package/dist/heart/habits/habit-runtime-state.js +17 -3
  12. package/dist/heart/habits/habit-scheduler.js +24 -5
  13. package/dist/heart/habits/habit-session-summary.js +318 -0
  14. package/dist/heart/habits/habit-session.js +618 -0
  15. package/dist/heart/mailbox/mailbox-http-hooks.js +29 -1
  16. package/dist/heart/mailbox/mailbox-http-routes.js +122 -1
  17. package/dist/heart/mailbox/mailbox-read.js +5 -1
  18. package/dist/heart/mailbox/readers/runtime-readers.js +87 -0
  19. package/dist/mailbox-ui/assets/index-CaTIFDmv.js +1 -0
  20. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  21. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  22. package/dist/mailbox-ui/index.html +3 -2
  23. package/dist/repertoire/tools-notes.js +50 -0
  24. package/dist/repertoire/tools-record.js +13 -0
  25. package/dist/repertoire/tools-session.js +140 -0
  26. package/dist/repertoire/tools-surface.js +11 -0
  27. package/dist/repertoire/tools.js +7 -0
  28. package/dist/senses/habit-turn-message.js +41 -3
  29. package/dist/senses/inner-dialog-worker.js +264 -68
  30. package/dist/senses/inner-dialog.js +29 -15
  31. package/dist/senses/pipeline.js +2 -11
  32. package/dist/senses/surface-tool.js +2 -1
  33. package/package.json +1 -1
  34. package/dist/mailbox-ui/assets/index-BZ60na8O.js +0 -61
  35. package/dist/mailbox-ui/assets/index-DG6Xf5uL.css +0 -1
package/changelog.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.667",
6
+ "changes": [
7
+ "Add stateful habit session summaries, summary tooling, mailbox APIs, nerves review, and habit history visibility.",
8
+ "Harden habit summary receipts as the canonical summary snapshot, validate agent-scoped Mailbox routes, await post-turn persistence, and preserve session-summary recovery through malformed locators and projected session edge cases."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.666",
13
+ "changes": [
14
+ "Close pre-merge habit session review gaps for route gating, executable risk policy, and Desk produced refs."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.665",
6
19
  "changes": [
@@ -33,11 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isHabitRunTrigger = isHabitRunTrigger;
36
37
  exports.flightRecorderLatestPath = flightRecorderLatestPath;
37
38
  exports.isFlightRecorderResume = isFlightRecorderResume;
38
39
  exports.readFlightRecorderResume = readFlightRecorderResume;
39
40
  exports.writeFlightRecorderResume = writeFlightRecorderResume;
40
41
  exports.recordFlightRecorderEvent = recordFlightRecorderEvent;
42
+ exports.isSafeHabitRunId = isSafeHabitRunId;
43
+ exports.readHabitRunReceipt = readHabitRunReceipt;
44
+ exports.listHabitRunReceipts = listHabitRunReceipts;
41
45
  exports.writeHabitRunReceipt = writeHabitRunReceipt;
42
46
  exports.formatFlightRecorderResume = formatFlightRecorderResume;
43
47
  exports.createHabitRunId = createHabitRunId;
@@ -46,6 +50,13 @@ const path = __importStar(require("path"));
46
50
  const crypto_1 = require("crypto");
47
51
  const session_events_1 = require("../heart/session-events");
48
52
  const runtime_1 = require("../nerves/runtime");
53
+ function isHabitRunTrigger(value) {
54
+ return value === "cron"
55
+ || value === "launchd"
56
+ || value === "poke"
57
+ || value === "overdue"
58
+ || value === "manual";
59
+ }
49
60
  function flightRecorderDir(agentRoot) {
50
61
  return path.join(agentRoot, "arc", "flight-recorder");
51
62
  }
@@ -55,6 +66,9 @@ function eventsDir(agentRoot) {
55
66
  function receiptsDir(agentRoot) {
56
67
  return path.join(flightRecorderDir(agentRoot), "habit-receipts");
57
68
  }
69
+ function habitReceiptPath(agentRoot, runId) {
70
+ return path.join(receiptsDir(agentRoot), `${runId}.json`);
71
+ }
58
72
  function flightRecorderLatestPath(agentRoot) {
59
73
  return path.join(flightRecorderDir(agentRoot), "latest.json");
60
74
  }
@@ -311,26 +325,331 @@ function recordFlightRecorderEvent(agentRoot, input) {
311
325
  });
312
326
  return event;
313
327
  }
314
- function writeHabitRunReceipt(agentRoot, receipt) {
315
- fs.mkdirSync(receiptsDir(agentRoot), { recursive: true });
316
- const safeReceipt = {
328
+ function isSafeHabitRunId(value) {
329
+ return typeof value === "string"
330
+ && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(value)
331
+ && !value.includes("..");
332
+ }
333
+ function isPlainRecord(value) {
334
+ return !!value && typeof value === "object" && !Array.isArray(value);
335
+ }
336
+ function isProducedRefArray(value) {
337
+ return Array.isArray(value)
338
+ && value.every((entry) => isPlainRecord(entry)
339
+ && (entry.kind === "arc"
340
+ || entry.kind === "desk_task"
341
+ || entry.kind === "desk_record"
342
+ || entry.kind === "claim"
343
+ || entry.kind === "surface"
344
+ || entry.kind === "none")
345
+ && typeof entry.locator === "string");
346
+ }
347
+ function isHabitSurfaceAttemptArray(value) {
348
+ return Array.isArray(value)
349
+ && value.every((entry) => isPlainRecord(entry)
350
+ && typeof entry.recipient === "string"
351
+ && typeof entry.channel === "string"
352
+ && (entry.reason === "needed_input"
353
+ || entry.reason === "status"
354
+ || entry.reason === "answer"
355
+ || entry.reason === "blocked"
356
+ || entry.reason === "other")
357
+ && (entry.result === "sent"
358
+ || entry.result === "delivered"
359
+ || entry.result === "delivered_now"
360
+ || entry.result === "queued"
361
+ || entry.result === "deferred"
362
+ || entry.result === "blocked"
363
+ || entry.result === "failed"
364
+ || entry.result === "unavailable")
365
+ && (entry.routeKind === undefined
366
+ || entry.routeKind === "family"
367
+ || entry.routeKind === "originator"
368
+ || entry.routeKind === "extra")
369
+ && (entry.rawStatus === undefined || typeof entry.rawStatus === "string")
370
+ && (entry.error === undefined || typeof entry.error === "string"));
371
+ }
372
+ function isHabitReturnRouteArray(value) {
373
+ return Array.isArray(value)
374
+ && value.every((entry) => isPlainRecord(entry)
375
+ && (entry.kind === "family" || entry.kind === "originator" || entry.kind === "extra")
376
+ && typeof entry.recipient === "string"
377
+ && (entry.status === "allowed" || entry.status === "unresolved")
378
+ && (entry.friendId === undefined || typeof entry.friendId === "string")
379
+ && (entry.channel === undefined || typeof entry.channel === "string")
380
+ && (entry.key === undefined || typeof entry.key === "string")
381
+ && (entry.reason === undefined || typeof entry.reason === "string"));
382
+ }
383
+ function isHabitPermissionEnvelope(value) {
384
+ if (!isPlainRecord(value))
385
+ return false;
386
+ return value.schemaVersion === 1
387
+ && typeof value.canMessageOutward === "boolean"
388
+ && isHabitReturnRouteArray(value.returnRoutes)
389
+ && isStringArray(value.deniedTools)
390
+ && isStringArray(value.warnings);
391
+ }
392
+ function isHabitToolPolicy(value) {
393
+ if (!isPlainRecord(value))
394
+ return false;
395
+ return (value.requestedTools === null || isStringArray(value.requestedTools))
396
+ && isStringArray(value.grantedTools)
397
+ && isStringArray(value.deniedTools)
398
+ && typeof value.outwardMessagingAllowed === "boolean";
399
+ }
400
+ function defaultHabitRunSummarySnapshot(receipt) {
401
+ if (receipt.errors.length > 0) {
402
+ return {
403
+ summary: `Habit ${receipt.habitName} finished with errors: ${receipt.errors.join("; ")}`,
404
+ decisions: [],
405
+ nextLikelyStep: null,
406
+ };
407
+ }
408
+ const surface = receipt.surfaceAttempts.find((attempt) => attempt.result !== "blocked" && attempt.result !== "failed" && attempt.result !== "unavailable");
409
+ if (surface) {
410
+ return {
411
+ summary: `Habit ${receipt.habitName} surfaced via ${surface.recipient}/${surface.channel}.`,
412
+ decisions: [],
413
+ nextLikelyStep: null,
414
+ };
415
+ }
416
+ const produced = receipt.producedRefs.find((ref) => ref.kind !== "none");
417
+ if (produced) {
418
+ return {
419
+ summary: `Habit ${receipt.habitName} produced ${produced.kind}: ${produced.locator}.`,
420
+ decisions: [],
421
+ nextLikelyStep: null,
422
+ };
423
+ }
424
+ return {
425
+ summary: `Habit ${receipt.habitName} finished with ${receipt.outcome}.`,
426
+ decisions: [],
427
+ nextLikelyStep: null,
428
+ };
429
+ }
430
+ function normalizeHabitRunSummarySnapshot(value, fallback) {
431
+ const snapshot = isPlainRecord(value) ? value : {};
432
+ const summary = typeof snapshot.summary === "string" && snapshot.summary.trim().length > 0
433
+ ? snapshot.summary
434
+ : fallback.summary;
435
+ const nextLikelyStep = snapshot.nextLikelyStep === null
436
+ ? null
437
+ : typeof snapshot.nextLikelyStep === "string" && snapshot.nextLikelyStep.trim().length > 0
438
+ ? snapshot.nextLikelyStep
439
+ : fallback.nextLikelyStep;
440
+ return {
441
+ summary: (0, session_events_1.capStructuredRecordString)(summary),
442
+ decisions: cappedArray(isStringArray(snapshot.decisions) ? snapshot.decisions : fallback.decisions),
443
+ nextLikelyStep: nextLikelyStep === null ? null : (0, session_events_1.capStructuredRecordString)(nextLikelyStep),
444
+ };
445
+ }
446
+ function isHabitRunReceipt(value) {
447
+ if (!isPlainRecord(value))
448
+ return false;
449
+ return value.schemaVersion === 2
450
+ && isSafeHabitRunId(value.runId)
451
+ && typeof value.sessionId === "string"
452
+ && typeof value.habitName === "string"
453
+ && (value.trigger === "cron"
454
+ || value.trigger === "launchd"
455
+ || value.trigger === "poke"
456
+ || value.trigger === "overdue"
457
+ || value.trigger === "manual")
458
+ && typeof value.startedAt === "string"
459
+ && typeof value.endedAt === "string"
460
+ && (value.outcome === "no_change"
461
+ || value.outcome === "wrote_arc"
462
+ || value.outcome === "updated_desk"
463
+ || value.outcome === "wrote_record"
464
+ || value.outcome === "surfaced"
465
+ || value.outcome === "blocked"
466
+ || value.outcome === "error")
467
+ && typeof value.definitionLocator === "string"
468
+ && typeof value.sessionLocator === "string"
469
+ && typeof value.pendingLocator === "string"
470
+ && typeof value.runtimeStateLocator === "string"
471
+ && typeof value.receiptLocator === "string"
472
+ && (value.operationId === undefined || value.operationId === null || typeof value.operationId === "string")
473
+ && (value.nextRunAt === null || typeof value.nextRunAt === "string")
474
+ && isHabitPermissionEnvelope(value.permissionEnvelope)
475
+ && isHabitToolPolicy(value.toolPolicy)
476
+ && (value.summarySnapshot === undefined || isPlainRecord(value.summarySnapshot))
477
+ && isProducedRefArray(value.producedRefs)
478
+ && isHabitSurfaceAttemptArray(value.surfaceAttempts)
479
+ && isStringArray(value.errors);
480
+ }
481
+ function isLegacyHabitRunReceipt(value) {
482
+ if (!isPlainRecord(value))
483
+ return false;
484
+ return value.schemaVersion === 1
485
+ && isSafeHabitRunId(value.runId)
486
+ && typeof value.habitName === "string"
487
+ && (value.trigger === "cron"
488
+ || value.trigger === "launchd"
489
+ || value.trigger === "poke"
490
+ || value.trigger === "overdue"
491
+ || value.trigger === "manual")
492
+ && typeof value.startedAt === "string"
493
+ && typeof value.endedAt === "string"
494
+ && (value.outcome === "no_change"
495
+ || value.outcome === "wrote_arc"
496
+ || value.outcome === "updated_desk"
497
+ || value.outcome === "wrote_record"
498
+ || value.outcome === "surfaced"
499
+ || value.outcome === "blocked"
500
+ || value.outcome === "error")
501
+ && isProducedRefArray(value.producedRefs)
502
+ && isHabitSurfaceAttemptArray(value.surfaceAttempts)
503
+ && isStringArray(value.errors);
504
+ }
505
+ function warnMalformedHabitReceipt(agentRoot, runId, reason) {
506
+ (0, runtime_1.emitNervesEvent)({
507
+ level: "warn",
508
+ component: "mind",
509
+ event: "mind.flight_recorder_habit_receipt_malformed",
510
+ message: "flight recorder habit receipt malformed",
511
+ meta: { agentRoot, runId: (0, session_events_1.capStructuredRecordString)(runId), reason },
512
+ });
513
+ }
514
+ function normalizeLegacyHabitRunReceipt(receipt) {
515
+ const sawSurface = receipt.surfaceAttempts.length > 0 || receipt.producedRefs.some((ref) => ref.kind === "surface");
516
+ return {
517
+ schemaVersion: 2,
518
+ runId: receipt.runId,
519
+ sessionId: receipt.runId,
520
+ habitName: receipt.habitName,
521
+ trigger: receipt.trigger,
522
+ startedAt: receipt.startedAt,
523
+ endedAt: receipt.endedAt,
524
+ outcome: receipt.outcome,
525
+ definitionLocator: `habits/${receipt.habitName}.md`,
526
+ sessionLocator: `state/habit-sessions/${receipt.runId}/session.json`,
527
+ pendingLocator: `state/habit-sessions/${receipt.runId}/pending`,
528
+ runtimeStateLocator: `state/habits/${receipt.habitName}.json`,
529
+ receiptLocator: `arc/flight-recorder/habit-receipts/${receipt.runId}.json`,
530
+ operationId: null,
531
+ nextRunAt: null,
532
+ permissionEnvelope: {
533
+ schemaVersion: 1,
534
+ canMessageOutward: sawSurface,
535
+ returnRoutes: [],
536
+ deniedTools: sawSurface ? [] : ["send_message", "surface"],
537
+ warnings: ["legacy receipt normalized without habit permission envelope"],
538
+ },
539
+ toolPolicy: {
540
+ requestedTools: null,
541
+ grantedTools: sawSurface ? ["surface"] : [],
542
+ deniedTools: sawSurface ? [] : ["send_message", "surface"],
543
+ outwardMessagingAllowed: sawSurface,
544
+ },
545
+ summarySnapshot: defaultHabitRunSummarySnapshot(receipt),
546
+ producedRefs: receipt.producedRefs,
547
+ surfaceAttempts: receipt.surfaceAttempts,
548
+ errors: receipt.errors,
549
+ };
550
+ }
551
+ function capHabitRunReceipt(receipt) {
552
+ const fallbackSnapshot = defaultHabitRunSummarySnapshot(receipt);
553
+ return {
317
554
  ...receipt,
318
555
  habitName: (0, session_events_1.capStructuredRecordString)(receipt.habitName),
556
+ definitionLocator: (0, session_events_1.capStructuredRecordString)(receipt.definitionLocator),
557
+ sessionLocator: (0, session_events_1.capStructuredRecordString)(receipt.sessionLocator),
558
+ pendingLocator: (0, session_events_1.capStructuredRecordString)(receipt.pendingLocator),
559
+ runtimeStateLocator: (0, session_events_1.capStructuredRecordString)(receipt.runtimeStateLocator),
560
+ receiptLocator: (0, session_events_1.capStructuredRecordString)(receipt.receiptLocator),
561
+ operationId: receipt.operationId ? (0, session_events_1.capStructuredRecordString)(receipt.operationId) : null,
562
+ permissionEnvelope: {
563
+ ...receipt.permissionEnvelope,
564
+ returnRoutes: receipt.permissionEnvelope.returnRoutes.map((route) => ({
565
+ ...route,
566
+ recipient: (0, session_events_1.capStructuredRecordString)(route.recipient),
567
+ ...(route.friendId ? { friendId: (0, session_events_1.capStructuredRecordString)(route.friendId) } : {}),
568
+ ...(route.channel ? { channel: (0, session_events_1.capStructuredRecordString)(route.channel) } : {}),
569
+ ...(route.key ? { key: (0, session_events_1.capStructuredRecordString)(route.key) } : {}),
570
+ ...(route.reason ? { reason: (0, session_events_1.capStructuredRecordString)(route.reason) } : {}),
571
+ })),
572
+ deniedTools: cappedArray(receipt.permissionEnvelope.deniedTools),
573
+ warnings: cappedArray(receipt.permissionEnvelope.warnings),
574
+ },
575
+ toolPolicy: {
576
+ requestedTools: receipt.toolPolicy.requestedTools ? cappedArray(receipt.toolPolicy.requestedTools) : null,
577
+ grantedTools: cappedArray(receipt.toolPolicy.grantedTools),
578
+ deniedTools: cappedArray(receipt.toolPolicy.deniedTools),
579
+ outwardMessagingAllowed: receipt.toolPolicy.outwardMessagingAllowed,
580
+ },
581
+ summarySnapshot: normalizeHabitRunSummarySnapshot(receipt.summarySnapshot, fallbackSnapshot),
319
582
  producedRefs: receipt.producedRefs.map((ref) => ({ ...ref, locator: (0, session_events_1.capStructuredRecordString)(ref.locator) })),
320
583
  surfaceAttempts: receipt.surfaceAttempts.map((attempt) => ({
321
584
  ...attempt,
322
585
  recipient: (0, session_events_1.capStructuredRecordString)(attempt.recipient),
323
586
  channel: (0, session_events_1.capStructuredRecordString)(attempt.channel),
587
+ ...(attempt.rawStatus ? { rawStatus: (0, session_events_1.capStructuredRecordString)(attempt.rawStatus) } : {}),
588
+ ...(attempt.error ? { error: (0, session_events_1.capStructuredRecordString)(attempt.error) } : {}),
324
589
  })),
325
590
  errors: receipt.errors.map((error) => (0, session_events_1.capStructuredRecordString)(error)),
326
591
  };
327
- atomicWriteJson(path.join(receiptsDir(agentRoot), `${safeReceipt.runId}.json`), safeReceipt);
592
+ }
593
+ function normalizeHabitRunReceiptForWrite(receipt) {
594
+ return capHabitRunReceipt(receipt.schemaVersion === 1 ? normalizeLegacyHabitRunReceipt(receipt) : receipt);
595
+ }
596
+ function readHabitRunReceipt(agentRoot, runId) {
597
+ if (!isSafeHabitRunId(runId)) {
598
+ warnMalformedHabitReceipt(agentRoot, runId, "unsafe run id");
599
+ return null;
600
+ }
601
+ try {
602
+ const parsed = JSON.parse(fs.readFileSync(habitReceiptPath(agentRoot, runId), "utf-8"));
603
+ const receipt = isHabitRunReceipt(parsed)
604
+ ? parsed
605
+ : isLegacyHabitRunReceipt(parsed)
606
+ ? normalizeLegacyHabitRunReceipt(parsed)
607
+ : null;
608
+ if (!receipt) {
609
+ warnMalformedHabitReceipt(agentRoot, runId, "invalid habit receipt shape");
610
+ return null;
611
+ }
612
+ (0, runtime_1.emitNervesEvent)({
613
+ component: "mind",
614
+ event: "mind.flight_recorder_habit_receipt_read",
615
+ message: "flight recorder habit receipt read",
616
+ meta: { agentRoot, runId },
617
+ });
618
+ return capHabitRunReceipt(receipt);
619
+ }
620
+ catch (error) {
621
+ warnMalformedHabitReceipt(agentRoot, runId, error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error));
622
+ return null;
623
+ }
624
+ }
625
+ function listHabitRunReceipts(agentRoot, options = {}) {
626
+ const dir = receiptsDir(agentRoot);
627
+ if (!fs.existsSync(dir))
628
+ return [];
629
+ const receipts = fs.readdirSync(dir)
630
+ .filter((fileName) => fileName.endsWith(".json"))
631
+ .map((fileName) => readHabitRunReceipt(agentRoot, path.basename(fileName, ".json")))
632
+ .filter((receipt) => receipt !== null)
633
+ .sort((left, right) => right.endedAt.localeCompare(left.endedAt) || right.runId.localeCompare(left.runId));
634
+ return typeof options.limit === "number" && options.limit >= 0 ? receipts.slice(0, options.limit) : receipts;
635
+ }
636
+ function writeHabitRunReceipt(agentRoot, receipt) {
637
+ fs.mkdirSync(receiptsDir(agentRoot), { recursive: true });
638
+ const safeReceipt = normalizeHabitRunReceiptForWrite(receipt);
639
+ if (!isSafeHabitRunId(safeReceipt.runId)) {
640
+ warnMalformedHabitReceipt(agentRoot, safeReceipt.runId, "unsafe run id");
641
+ throw new Error(`unsafe habit run id: ${safeReceipt.runId}`);
642
+ }
643
+ atomicWriteJson(habitReceiptPath(agentRoot, safeReceipt.runId), safeReceipt);
328
644
  recordFlightRecorderEvent(agentRoot, {
329
645
  kind: "habit_run",
330
646
  recordedAt: safeReceipt.endedAt,
331
647
  summary: `habit ${safeReceipt.habitName} finished with ${safeReceipt.outcome}`,
332
648
  producedRefs: safeReceipt.producedRefs,
333
- meta: { receiptPath: path.join("arc", "flight-recorder", "habit-receipts", `${safeReceipt.runId}.json`) },
649
+ meta: {
650
+ receiptPath: path.join("arc", "flight-recorder", "habit-receipts", `${safeReceipt.runId}.json`),
651
+ operationId: safeReceipt.operationId ?? null,
652
+ },
334
653
  });
335
654
  (0, runtime_1.emitNervesEvent)({
336
655
  component: "mind",
@@ -6,6 +6,7 @@ exports.getModel = getModel;
6
6
  exports.getProvider = getProvider;
7
7
  exports.createSummarize = createSummarize;
8
8
  exports.getProviderDisplayLabel = getProviderDisplayLabel;
9
+ exports.createHabitCallbackBuffer = createHabitCallbackBuffer;
9
10
  exports.isChatStyleChannel = isChatStyleChannel;
10
11
  exports.buildPonderResult = buildPonderResult;
11
12
  exports.isExternalStateQuery = isExternalStateQuery;
@@ -36,6 +37,7 @@ const packets_1 = require("../arc/packets");
36
37
  const tool_friction_1 = require("./tool-friction");
37
38
  const provider_models_1 = require("./provider-models");
38
39
  const provider_credentials_1 = require("./provider-credentials");
40
+ const habit_session_1 = require("./habits/habit-session");
39
41
  const provider_attempt_1 = require("./provider-attempt");
40
42
  const openai_codex_token_1 = require("./providers/openai-codex-token");
41
43
  const _providerRuntimes = {
@@ -185,6 +187,43 @@ function getProviderDisplayLabel(facing = "human") {
185
187
  };
186
188
  return providerLabelBuilders[provider]();
187
189
  }
190
+ function createHabitCallbackBuffer(callbacks) {
191
+ const events = [];
192
+ return {
193
+ callbacks: {
194
+ ...callbacks,
195
+ onModelStreamStart: () => { events.push({ kind: "stream-start" }); },
196
+ onTextChunk: (text) => { events.push({ kind: "text", text }); },
197
+ onReasoningChunk: (text) => { events.push({ kind: "reasoning", text }); },
198
+ onClearText: () => { events.push({ kind: "clear" }); },
199
+ flushNow: () => { events.push({ kind: "flush" }); },
200
+ },
201
+ async flush() {
202
+ for (const event of events.splice(0)) {
203
+ switch (event.kind) {
204
+ case "stream-start":
205
+ callbacks.onModelStreamStart();
206
+ break;
207
+ case "text":
208
+ callbacks.onTextChunk(event.text);
209
+ break;
210
+ case "reasoning":
211
+ callbacks.onReasoningChunk(event.text);
212
+ break;
213
+ case "clear":
214
+ callbacks.onClearText?.();
215
+ break;
216
+ case "flush":
217
+ await callbacks.flushNow?.();
218
+ break;
219
+ }
220
+ }
221
+ },
222
+ discard() {
223
+ events.splice(0);
224
+ },
225
+ };
226
+ }
188
227
  /**
189
228
  * Strip <think>...</think> blocks for the violation-detection check at the
190
229
  * end of a streaming turn. Used to tell legitimate text-only responses
@@ -215,6 +254,85 @@ function hasFreshPendingWork(options) {
215
254
  return pendingMessages.some((message) => typeof message?.content === "string"
216
255
  && message.content.trim().length > 0);
217
256
  }
257
+ const HABIT_CONTROL_TOOLS = new Set(["rest", "ponder", "observe"]);
258
+ function habitToolArgs(call) {
259
+ try {
260
+ const parsed = JSON.parse(call.arguments);
261
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
262
+ return { ok: false, reason: `habit tool '${call.name}' arguments must be a JSON object` };
263
+ }
264
+ return { ok: true, args: parsed };
265
+ }
266
+ catch {
267
+ return { ok: false, reason: `habit tool '${call.name}' has malformed JSON arguments` };
268
+ }
269
+ }
270
+ function highRiskExternalMutation(profile) {
271
+ if (profile.risk !== "high")
272
+ return null;
273
+ const mutates = typeof profile.mutates === "string" ? [profile.mutates] : [...profile.mutates];
274
+ return mutates.includes("external_side_effect") ? mutates.join(", ") : null;
275
+ }
276
+ function recordBlockedHabitSurfaceAttempts(habitSession, toolCalls, reason) {
277
+ if (toolCalls.some((call) => call.name !== "send_message" && call.name !== "surface")) {
278
+ habitSession?.recordError?.(`blocked habit tool batch: ${reason}`);
279
+ }
280
+ if (!habitSession?.recordSurfaceAttempt)
281
+ return;
282
+ for (const call of toolCalls) {
283
+ if (call.name !== "send_message" && call.name !== "surface")
284
+ continue;
285
+ const parsed = habitToolArgs(call);
286
+ const args = parsed.ok ? parsed.args : {};
287
+ habitSession.recordSurfaceAttempt({
288
+ recipient: String(args.friendId ?? args.delegationId ?? "unknown"),
289
+ channel: String(args.channel ?? call.name),
290
+ reason: "blocked",
291
+ result: "blocked",
292
+ rawStatus: "blocked",
293
+ error: reason,
294
+ });
295
+ }
296
+ }
297
+ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigins, activeToolNames) {
298
+ if (!habitSession)
299
+ return null;
300
+ const granted = new Set(habitSession.toolPolicy.grantedTools);
301
+ const denied = new Set(habitSession.toolPolicy.deniedTools);
302
+ for (const call of toolCalls) {
303
+ if (denied.has(call.name))
304
+ return `habit tool '${call.name}' is denied by this habit session`;
305
+ if (!activeToolNames.has(call.name))
306
+ return `habit tool '${call.name}' was not advertised to this model turn`;
307
+ if (HABIT_CONTROL_TOOLS.has(call.name))
308
+ continue;
309
+ if (!granted.has(call.name))
310
+ return `habit tool '${call.name}' was not granted to this habit session`;
311
+ const parsed = habitToolArgs(call);
312
+ if (!parsed.ok)
313
+ return parsed.reason;
314
+ const riskProfile = (0, tools_1.riskProfileForToolName)(call.name, parsed.args);
315
+ if (!riskProfile)
316
+ return `habit tool '${call.name}' does not have a known executable risk profile`;
317
+ const externalMutation = highRiskExternalMutation(riskProfile);
318
+ if (externalMutation && call.name !== "send_message" && call.name !== "surface") {
319
+ return `habit tool '${call.name}' has high-risk executable mutation ${externalMutation}: ${riskProfile.reason}`;
320
+ }
321
+ if (call.name === "send_message" || call.name === "surface") {
322
+ const route = await (0, habit_session_1.resolveHabitReturnRoute)({
323
+ agentRoot: (0, identity_2.getAgentRoot)(),
324
+ envelope: habitSession.permissionEnvelope,
325
+ toolName: call.name,
326
+ args: parsed.args,
327
+ friendStore: habitSession.friendStore,
328
+ delegatedOrigins,
329
+ });
330
+ if (!route.allowed)
331
+ return route.reason;
332
+ }
333
+ }
334
+ return null;
335
+ }
218
336
  /** Chat-style channels expose the `speak` tool — outer human-conversation channels
219
337
  * where mid-turn delivery is meaningful. Inner dialog has `ponder`. MCP returns
220
338
  * synchronously. Mail is batch. Anything else (unknown channel) treats as non-chat. */
@@ -744,6 +862,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
744
862
  // Augment tool context with reasoning effort controls from provider
745
863
  const baseToolContext = options?.toolContext
746
864
  ?? (options?.orientationFrame ? { signin: async () => undefined, orientationFrame: options.orientationFrame } : undefined);
865
+ const habitSession = options?.habitSession ?? baseToolContext?.habitSession;
747
866
  const augmentedToolContext = baseToolContext
748
867
  ? {
749
868
  ...baseToolContext,
@@ -751,8 +870,16 @@ async function runAgent(messages, callbacks, channel, signal, options) {
751
870
  setReasoningEffort: (level) => { currentReasoningEffort = level; },
752
871
  activeWorkFrame: options?.activeWorkFrame,
753
872
  orientationFrame: options?.orientationFrame ?? baseToolContext.orientationFrame,
873
+ ...(habitSession ? { habitSession } : {}),
754
874
  }
755
- : undefined;
875
+ : habitSession
876
+ ? {
877
+ signin: async () => undefined,
878
+ habitSession,
879
+ supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
880
+ setReasoningEffort: (level) => { currentReasoningEffort = level; },
881
+ }
882
+ : undefined;
756
883
  // Rebase provider-owned turn state from canonical messages at user-turn start.
757
884
  // This prevents stale provider caches from replaying prior-turn context.
758
885
  providerRuntime.resetTurnState(messages);
@@ -766,17 +893,25 @@ async function runAgent(messages, callbacks, channel, signal, options) {
766
893
  // Inner dialog gets restTool instead of settleTool (rest = end turn, gated by attention queue).
767
894
  // toolChoiceRequired only controls whether tool_choice: "required" is set in the API call.
768
895
  const isInnerDialog = channel === "inner";
896
+ const innerHabitCanSendMessage = isInnerDialog
897
+ && habitSession?.toolPolicy.outwardMessagingAllowed === true
898
+ && habitSession.toolPolicy.grantedTools.includes("send_message");
899
+ const innerHabitCanSurface = isInnerDialog
900
+ && (!habitSession || (habitSession.toolPolicy.outwardMessagingAllowed === true
901
+ && habitSession.toolPolicy.grantedTools.includes("surface")));
769
902
  const filteredBaseTools = isInnerDialog
770
- ? baseTools.filter((t) => t.function.name !== "send_message")
903
+ ? baseTools.filter((t) => innerHabitCanSendMessage || t.function.name !== "send_message")
771
904
  : baseTools;
772
905
  const activeTools = [
773
906
  ...filteredBaseTools,
774
907
  tools_1.ponderTool,
775
- ...(isInnerDialog ? [tools_2.surfaceToolDef, tools_1.restTool] : []),
908
+ ...(isInnerDialog && innerHabitCanSurface ? [tools_2.surfaceToolDef] : []),
909
+ ...(isInnerDialog ? [tools_1.restTool] : []),
776
910
  ...(!isInnerDialog ? [tools_1.observeTool] : []),
777
911
  ...(!isInnerDialog ? [tools_1.settleTool] : []),
778
912
  ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
779
913
  ];
914
+ const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
780
915
  const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
781
916
  if (steeringFollowUps.length > 0) {
782
917
  const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
@@ -803,13 +938,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
803
938
  break;
804
939
  }
805
940
  try {
941
+ const habitCallbackBufferRef = { current: null };
806
942
  const callProviderTurn = async () => {
807
943
  callbacks.onModelStart();
944
+ habitCallbackBufferRef.current = habitSession ? createHabitCallbackBuffer(callbacks) : null;
808
945
  try {
809
946
  return await providerRuntime.streamTurn({
810
947
  messages,
811
948
  activeTools,
812
- callbacks,
949
+ callbacks: habitCallbackBufferRef.current?.callbacks ?? callbacks,
813
950
  signal,
814
951
  traceId,
815
952
  toolChoiceRequired,
@@ -819,6 +956,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
819
956
  });
820
957
  }
821
958
  catch (error) {
959
+ habitCallbackBufferRef.current?.discard();
960
+ habitCallbackBufferRef.current = null;
822
961
  if (signal?.aborted)
823
962
  throw new provider_attempt_1.ProviderAttemptAbortError();
824
963
  throw error;
@@ -890,6 +1029,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
890
1029
  continue;
891
1030
  }
892
1031
  const result = attempt.value;
1032
+ const streamCallbackBuffer = habitCallbackBufferRef.current;
1033
+ habitCallbackBufferRef.current = null;
893
1034
  // Track usage from the latest API call
894
1035
  if (result.usage)
895
1036
  lastUsage = result.usage;
@@ -968,6 +1109,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
968
1109
  })
969
1110
  : null;
970
1111
  if (!result.toolCalls.length) {
1112
+ await streamCallbackBuffer?.flush();
971
1113
  if (privateReturnTextAckRetryError) {
972
1114
  callbacks.onClearText?.();
973
1115
  if (noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
@@ -1050,6 +1192,26 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1050
1192
  else {
1051
1193
  // Reset the retry counter on any successful tool call.
1052
1194
  noToolCallRetries = 0;
1195
+ const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins, activeToolNames);
1196
+ if (habitBlockReason) {
1197
+ streamCallbackBuffer?.discard();
1198
+ recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
1199
+ messages.push(msg);
1200
+ const blockedOutput = `blocked: ${habitBlockReason}. No tool side effects from this assistant message were executed.`;
1201
+ for (const call of result.toolCalls) {
1202
+ messages.push({ role: "tool", tool_call_id: call.id, content: blockedOutput });
1203
+ providerRuntime.appendToolOutput(call.id, blockedOutput);
1204
+ }
1205
+ (0, runtime_1.emitNervesEvent)({
1206
+ level: "warn",
1207
+ component: "engine",
1208
+ event: "engine.habit_tool_batch_blocked",
1209
+ message: "habit tool batch blocked before side effects",
1210
+ meta: { reason: habitBlockReason, toolCalls: result.toolCalls.map((call) => call.name) },
1211
+ });
1212
+ continue;
1213
+ }
1214
+ await streamCallbackBuffer?.flush();
1053
1215
  // Check for settle sole call: intercept before tool execution
1054
1216
  if (isSoleSettle) {
1055
1217
  /* v8 ignore next -- defensive: JSON.parse catch for malformed settle args @preserve */
@@ -1501,6 +1663,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1501
1663
  catch (e) {
1502
1664
  toolResult = `error: ${e}`;
1503
1665
  success = false;
1666
+ augmentedToolContext?.habitSession?.recordError?.(toolResult);
1504
1667
  }
1505
1668
  toolResult = (0, tool_friction_1.rewriteToolResultForModel)(tc.name, toolResult, toolFrictionLedger);
1506
1669
  (0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);