@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.
- package/changelog.json +13 -0
- package/dist/arc/flight-recorder.js +324 -5
- package/dist/heart/core.js +167 -4
- package/dist/heart/cross-chat-delivery.js +3 -2
- package/dist/heart/daemon/cli-exec.js +139 -1
- package/dist/heart/daemon/cli-help.js +13 -2
- package/dist/heart/daemon/cli-parse.js +138 -2
- package/dist/heart/daemon/daemon-entry.js +24 -5
- package/dist/heart/daemon/daemon.js +10 -1
- package/dist/heart/habits/habit-parser.js +8 -0
- package/dist/heart/habits/habit-runtime-state.js +17 -3
- package/dist/heart/habits/habit-scheduler.js +24 -5
- package/dist/heart/habits/habit-session-summary.js +318 -0
- package/dist/heart/habits/habit-session.js +618 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +29 -1
- package/dist/heart/mailbox/mailbox-http-routes.js +122 -1
- package/dist/heart/mailbox/mailbox-read.js +5 -1
- package/dist/heart/mailbox/readers/runtime-readers.js +87 -0
- package/dist/mailbox-ui/assets/index-CaTIFDmv.js +1 -0
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
- package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
- package/dist/mailbox-ui/index.html +3 -2
- package/dist/repertoire/tools-notes.js +50 -0
- package/dist/repertoire/tools-record.js +13 -0
- package/dist/repertoire/tools-session.js +140 -0
- package/dist/repertoire/tools-surface.js +11 -0
- package/dist/repertoire/tools.js +7 -0
- package/dist/senses/habit-turn-message.js +41 -3
- package/dist/senses/inner-dialog-worker.js +264 -68
- package/dist/senses/inner-dialog.js +29 -15
- package/dist/senses/pipeline.js +2 -11
- package/dist/senses/surface-tool.js +2 -1
- package/package.json +1 -1
- package/dist/mailbox-ui/assets/index-BZ60na8O.js +0 -61
- 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
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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: {
|
|
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",
|
package/dist/heart/core.js
CHANGED
|
@@ -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
|
-
:
|
|
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
|
|
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);
|