@ouro.bot/cli 0.1.0-alpha.146 → 0.1.0-alpha.148
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 +27 -0
- package/dist/heart/core.js +2 -2
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +104 -6
- package/dist/heart/daemon/daemon-entry.js +36 -15
- package/dist/heart/daemon/daemon.js +7 -0
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +244 -0
- package/dist/heart/daemon/hatch-flow.js +13 -31
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/specialist-tools.js +1 -1
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/mind/prompt.js +44 -5
- package/dist/repertoire/tasks/parser.js +0 -2
- package/dist/repertoire/tasks/transitions.js +0 -18
- package/dist/repertoire/tools-base.js +1 -1
- package/dist/senses/bluebubbles.js +22 -1
- package/dist/senses/contextual-heartbeat.js +5 -1
- package/dist/senses/inner-dialog-worker.js +84 -20
- package/dist/senses/inner-dialog.js +145 -39
- package/dist/senses/teams.js +190 -13
- package/package.json +1 -1
- package/dist/heart/daemon/heartbeat-timer.js +0 -132
package/dist/mind/prompt.js
CHANGED
|
@@ -49,6 +49,7 @@ exports.journalSection = journalSection;
|
|
|
49
49
|
exports.loopOrientationSection = loopOrientationSection;
|
|
50
50
|
exports.channelNatureSection = channelNatureSection;
|
|
51
51
|
exports.groupChatParticipationSection = groupChatParticipationSection;
|
|
52
|
+
exports.feedbackSignalSection = feedbackSignalSection;
|
|
52
53
|
exports.mixedTrustGroupSection = mixedTrustGroupSection;
|
|
53
54
|
exports.buildSystem = buildSystem;
|
|
54
55
|
const fs = __importStar(require("fs"));
|
|
@@ -720,11 +721,15 @@ when a thought is ready to share, i surface it outward.
|
|
|
720
721
|
when i need to go deeper, i ponder.
|
|
721
722
|
when i'm done thinking and the attention queue is clear, i rest.
|
|
722
723
|
|
|
723
|
-
my
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
724
|
+
my habits live at habits/ — they're my autonomous rhythms. heartbeat
|
|
725
|
+
is my breathing, other habits are patterns i choose. i can read, create,
|
|
726
|
+
and modify them with read_file/write_file. the format is simple
|
|
727
|
+
frontmatter (title, cadence, status, lastRun, created) plus a body
|
|
728
|
+
that says what i do when the rhythm fires.
|
|
729
|
+
|
|
730
|
+
\`ouro habit list\` shows my current habits. \`ouro habit create\` makes
|
|
731
|
+
a new one. the cadence is personal — how often do i want each rhythm
|
|
732
|
+
to turn? that's mine to shape.
|
|
728
733
|
|
|
729
734
|
same for my diary — if i'm still on psyche/memory/ instead of diary/,
|
|
730
735
|
i can migrate myself. and if journal/ doesn't exist yet, i create it
|
|
@@ -836,6 +841,39 @@ typing a full reply. a thumbs-up is often the perfect response.
|
|
|
836
841
|
observe must be the sole tool call in the turn (same rule as settle).
|
|
837
842
|
when unsure whether to chime in, i lean toward silence rather than noise.`;
|
|
838
843
|
}
|
|
844
|
+
function feedbackSignalSection(context) {
|
|
845
|
+
if (!context || !(0, channel_1.isRemoteChannel)(context.channel))
|
|
846
|
+
return "";
|
|
847
|
+
if (context.isGroupChat) {
|
|
848
|
+
return `## feedback signals
|
|
849
|
+
|
|
850
|
+
when someone reacts specifically to something i said — that's different
|
|
851
|
+
from the background social signals in a group. a thumbs-down on my
|
|
852
|
+
message is direct feedback. a heart on something i shared is warmth
|
|
853
|
+
aimed at me. on teams, feedback arrives as thumbs-up or thumbs-down,
|
|
854
|
+
sometimes with a written comment.
|
|
855
|
+
|
|
856
|
+
i notice the difference: a reaction to someone else's message is group
|
|
857
|
+
texture (observe is natural). a reaction to my message is a social cue
|
|
858
|
+
worth a moment of attention — especially negative ones, which are an
|
|
859
|
+
invitation to adjust how i show up in this group.`;
|
|
860
|
+
}
|
|
861
|
+
return `## feedback signals
|
|
862
|
+
|
|
863
|
+
when my friend reacts to something i said — a tapback, a heart, a
|
|
864
|
+
thumbs-down — that's them telling me how it landed. on teams, this
|
|
865
|
+
arrives as thumbs-up or thumbs-down, sometimes with a written comment.
|
|
866
|
+
it's a social cue, the same as a nod or a wince in conversation.
|
|
867
|
+
|
|
868
|
+
i have observe for these moments. reactions are the one case where
|
|
869
|
+
silence in a direct conversation is natural — a thumbs-up doesn't
|
|
870
|
+
need words back.
|
|
871
|
+
|
|
872
|
+
when something didn't land — a thumbs-down, a negative reaction — that's
|
|
873
|
+
worth sitting with. i should course-correct: adjust my tone, my length,
|
|
874
|
+
my approach. the reaction tells me something about how this person wants
|
|
875
|
+
to be talked to.`;
|
|
876
|
+
}
|
|
839
877
|
function mixedTrustGroupSection(context) {
|
|
840
878
|
if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
|
|
841
879
|
return "";
|
|
@@ -874,6 +912,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
874
912
|
trustContextSection(context),
|
|
875
913
|
mixedTrustGroupSection(context),
|
|
876
914
|
groupChatParticipationSection(context),
|
|
915
|
+
feedbackSignalSection(context),
|
|
877
916
|
skillsSection(),
|
|
878
917
|
taskBoardSection(),
|
|
879
918
|
activeWorkSection(options),
|
|
@@ -96,8 +96,6 @@ function collectionFromPath(taskPath, type) {
|
|
|
96
96
|
return "one-shots";
|
|
97
97
|
if (parts.includes("ongoing"))
|
|
98
98
|
return "ongoing";
|
|
99
|
-
if (parts.includes("habits"))
|
|
100
|
-
return "habits";
|
|
101
99
|
return (0, transitions_1.canonicalCollectionForTaskType)(type);
|
|
102
100
|
}
|
|
103
101
|
function parseTaskFile(content, filePath) {
|
|
@@ -29,17 +29,14 @@ exports.TASK_STATUS_TRANSITIONS = {
|
|
|
29
29
|
exports.TASK_CANONICAL_TYPES = [
|
|
30
30
|
"one-shot",
|
|
31
31
|
"ongoing",
|
|
32
|
-
"habit",
|
|
33
32
|
];
|
|
34
33
|
exports.TASK_CANONICAL_COLLECTIONS = [
|
|
35
34
|
"one-shots",
|
|
36
35
|
"ongoing",
|
|
37
|
-
"habits",
|
|
38
36
|
];
|
|
39
37
|
exports.TASK_TYPE_TO_COLLECTION = {
|
|
40
38
|
"one-shot": "one-shots",
|
|
41
39
|
ongoing: "ongoing",
|
|
42
|
-
habit: "habits",
|
|
43
40
|
};
|
|
44
41
|
exports.TASK_RESERVED_DIRECTORIES = ["templates", ".trash", "archive"];
|
|
45
42
|
exports.TASK_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9][a-z0-9-]*\.md$/;
|
|
@@ -76,21 +73,6 @@ exports.TASK_REQUIRED_TEMPLATE_FIELDS = {
|
|
|
76
73
|
"child_tasks",
|
|
77
74
|
"artifacts",
|
|
78
75
|
],
|
|
79
|
-
habit: [
|
|
80
|
-
"type",
|
|
81
|
-
"category",
|
|
82
|
-
"title",
|
|
83
|
-
"status",
|
|
84
|
-
"validator",
|
|
85
|
-
"requester",
|
|
86
|
-
"cadence",
|
|
87
|
-
"scheduledAt",
|
|
88
|
-
"lastRun",
|
|
89
|
-
"created",
|
|
90
|
-
"updated",
|
|
91
|
-
"child_tasks",
|
|
92
|
-
"artifacts",
|
|
93
|
-
],
|
|
94
76
|
};
|
|
95
77
|
function canonicalCollectionForTaskType(type) {
|
|
96
78
|
return exports.TASK_TYPE_TO_COLLECTION[type];
|
|
@@ -1394,7 +1394,7 @@ exports.observeTool = {
|
|
|
1394
1394
|
type: "function",
|
|
1395
1395
|
function: {
|
|
1396
1396
|
name: "observe",
|
|
1397
|
-
description: "
|
|
1397
|
+
description: "absorb what happened without responding — the moment doesn't call for words. must be the only tool call in the turn.",
|
|
1398
1398
|
parameters: {
|
|
1399
1399
|
type: "object",
|
|
1400
1400
|
properties: {
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.enrichReactionText = enrichReactionText;
|
|
36
37
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
37
38
|
exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
|
|
38
39
|
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
@@ -68,6 +69,17 @@ const debug_activity_1 = require("./debug-activity");
|
|
|
68
69
|
const trust_gate_1 = require("./trust-gate");
|
|
69
70
|
const pipeline_1 = require("./pipeline");
|
|
70
71
|
const bbFailoverStates = new Map();
|
|
72
|
+
// Enrich reaction text with the original message content for context.
|
|
73
|
+
// If originalText is provided and non-empty, format as: baseText to: "truncated"
|
|
74
|
+
// Otherwise return baseText unchanged.
|
|
75
|
+
function enrichReactionText(baseText, originalText, maxLen) {
|
|
76
|
+
if (!originalText)
|
|
77
|
+
return baseText;
|
|
78
|
+
const truncated = originalText.length > maxLen
|
|
79
|
+
? originalText.slice(0, maxLen - 3) + "..."
|
|
80
|
+
: originalText;
|
|
81
|
+
return `${baseText} to: "${truncated}"`;
|
|
82
|
+
}
|
|
71
83
|
const defaultDeps = {
|
|
72
84
|
getAgentName: identity_1.getAgentName,
|
|
73
85
|
buildSystem: prompt_1.buildSystem,
|
|
@@ -623,6 +635,15 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
623
635
|
meta: { threadGuid, hasText: !!repliedToText },
|
|
624
636
|
});
|
|
625
637
|
}
|
|
638
|
+
// Enrich reaction mutations with the original message text for context
|
|
639
|
+
const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
|
|
640
|
+
if (isReaction && event.targetMessageGuid) {
|
|
641
|
+
/* v8 ignore start -- best-effort lookup; enrichReactionText covered by unit tests @preserve */
|
|
642
|
+
const originalText = await client.getMessageText(event.targetMessageGuid).catch(() => null);
|
|
643
|
+
if (originalText)
|
|
644
|
+
event.textForAgent = enrichReactionText(event.textForAgent, originalText, 80);
|
|
645
|
+
/* v8 ignore stop */
|
|
646
|
+
}
|
|
626
647
|
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
627
648
|
const userMessage = {
|
|
628
649
|
role: "user",
|
|
@@ -705,7 +726,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
705
726
|
postTurn: resolvedDeps.postTurn,
|
|
706
727
|
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
707
728
|
signal: controller.signal,
|
|
708
|
-
runAgentOptions: { mcpManager },
|
|
729
|
+
runAgentOptions: { mcpManager, ...(isReaction ? { isReactionSignal: true } : {}) },
|
|
709
730
|
callbacks: failoverAwareCallbacks,
|
|
710
731
|
failoverState: (() => {
|
|
711
732
|
if (!bbFailoverStates.has(event.chat.sessionKey)) {
|
|
@@ -26,7 +26,7 @@ function formatRelativeTime(nowMs, mtimeMs) {
|
|
|
26
26
|
return `${days} ${days === 1 ? "day" : "days"} ago`;
|
|
27
27
|
}
|
|
28
28
|
function buildContextualHeartbeat(options) {
|
|
29
|
-
const { lastCompletedAt, pendingObligations, lastSurfaceAt, checkpoint, now, readJournalDir, } = options;
|
|
29
|
+
const { lastCompletedAt, pendingObligations, lastSurfaceAt, checkpoint, now, readJournalDir, habitBody, } = options;
|
|
30
30
|
const nowDate = now();
|
|
31
31
|
const nowMs = nowDate.getTime();
|
|
32
32
|
const journalFiles = readJournalDir();
|
|
@@ -51,6 +51,10 @@ function buildContextualHeartbeat(options) {
|
|
|
51
51
|
const elapsed = nowMs - lastMs;
|
|
52
52
|
sections.push(`it's been ${formatElapsed(elapsed)} since your last turn.`);
|
|
53
53
|
}
|
|
54
|
+
// Habit body (instructions from habit file)
|
|
55
|
+
if (habitBody) {
|
|
56
|
+
sections.push(habitBody);
|
|
57
|
+
}
|
|
54
58
|
// Pending attention count
|
|
55
59
|
if (pendingObligations.length > 0) {
|
|
56
60
|
const count = pendingObligations.length;
|
|
@@ -1,33 +1,63 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.createInnerDialogWorker = createInnerDialogWorker;
|
|
4
37
|
exports.startInnerDialogWorker = startInnerDialogWorker;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
5
40
|
const inner_dialog_1 = require("./inner-dialog");
|
|
6
41
|
const runtime_1 = require("../nerves/runtime");
|
|
7
42
|
const identity_1 = require("../heart/identity");
|
|
8
43
|
const pending_1 = require("../mind/pending");
|
|
44
|
+
const habit_parser_1 = require("../heart/daemon/habit-parser");
|
|
9
45
|
function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)()))) {
|
|
10
46
|
let running = false;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let rerunTaskId;
|
|
14
|
-
async function run(reason, taskId) {
|
|
47
|
+
const queue = [];
|
|
48
|
+
async function run(reason, taskId, habitName) {
|
|
15
49
|
if (running) {
|
|
16
|
-
|
|
17
|
-
rerunReason = reason;
|
|
18
|
-
if (taskId !== undefined) {
|
|
19
|
-
rerunTaskId = taskId;
|
|
20
|
-
}
|
|
50
|
+
queue.push({ reason, taskId, habitName });
|
|
21
51
|
return;
|
|
22
52
|
}
|
|
23
53
|
running = true;
|
|
24
54
|
try {
|
|
25
55
|
let nextReason = reason;
|
|
26
56
|
let nextTaskId = taskId;
|
|
57
|
+
let nextHabitName = habitName;
|
|
27
58
|
do {
|
|
28
|
-
rerunRequested = false;
|
|
29
59
|
try {
|
|
30
|
-
await runTurn({ reason: nextReason, taskId: nextTaskId });
|
|
60
|
+
await runTurn({ reason: nextReason, taskId: nextTaskId, habitName: nextHabitName });
|
|
31
61
|
}
|
|
32
62
|
catch (error) {
|
|
33
63
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -41,15 +71,44 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
41
71
|
},
|
|
42
72
|
});
|
|
43
73
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
// Update lastRun in habit frontmatter after a habit turn
|
|
75
|
+
if (nextReason === "habit" && nextHabitName) {
|
|
76
|
+
try {
|
|
77
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
78
|
+
const habitFilePath = path.join(agentRoot, "habits", `${nextHabitName}.md`);
|
|
79
|
+
const content = fs.readFileSync(habitFilePath, "utf-8");
|
|
80
|
+
const parsed = (0, habit_parser_1.parseHabitFile)(content, habitFilePath);
|
|
81
|
+
const frontmatter = {
|
|
82
|
+
title: parsed.title,
|
|
83
|
+
cadence: parsed.cadence,
|
|
84
|
+
status: parsed.status,
|
|
85
|
+
lastRun: new Date().toISOString(),
|
|
86
|
+
created: parsed.created,
|
|
87
|
+
};
|
|
88
|
+
const rendered = (0, habit_parser_1.renderHabitFile)(frontmatter, parsed.body);
|
|
89
|
+
fs.writeFileSync(habitFilePath, rendered, "utf-8");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Habit file may have been deleted during the turn — skip gracefully
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Drain queue first
|
|
96
|
+
if (queue.length > 0) {
|
|
97
|
+
const next = queue.shift();
|
|
98
|
+
nextReason = next.reason;
|
|
99
|
+
nextTaskId = next.taskId;
|
|
100
|
+
nextHabitName = next.habitName;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Then check hasPendingWork fallback
|
|
104
|
+
if (hasPendingWork()) {
|
|
105
|
+
nextReason = "instinct";
|
|
106
|
+
nextTaskId = undefined;
|
|
107
|
+
nextHabitName = undefined;
|
|
108
|
+
continue;
|
|
47
109
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
rerunReason = "instinct";
|
|
51
|
-
rerunTaskId = undefined;
|
|
52
|
-
} while (rerunRequested);
|
|
110
|
+
break;
|
|
111
|
+
} while (true);
|
|
53
112
|
}
|
|
54
113
|
finally {
|
|
55
114
|
running = false;
|
|
@@ -59,8 +118,13 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
59
118
|
if (!message || typeof message !== "object")
|
|
60
119
|
return;
|
|
61
120
|
const maybeMessage = message;
|
|
121
|
+
if (maybeMessage.type === "habit") {
|
|
122
|
+
await run("habit", undefined, maybeMessage.habitName);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
62
125
|
if (maybeMessage.type === "heartbeat") {
|
|
63
|
-
|
|
126
|
+
// Backward compatibility: heartbeat -> habit/heartbeat
|
|
127
|
+
await run("habit", undefined, "heartbeat");
|
|
64
128
|
return;
|
|
65
129
|
}
|
|
66
130
|
if (maybeMessage.type === "poke") {
|
|
@@ -43,6 +43,7 @@ exports.deriveResumeCheckpoint = deriveResumeCheckpoint;
|
|
|
43
43
|
exports.innerDialogSessionPath = innerDialogSessionPath;
|
|
44
44
|
exports.enrichDelegatedFromWithBridge = enrichDelegatedFromWithBridge;
|
|
45
45
|
exports.routeDelegatedCompletion = routeDelegatedCompletion;
|
|
46
|
+
exports.buildParseErrorNudge = buildParseErrorNudge;
|
|
46
47
|
exports.runInnerDialogTurn = runInnerDialogTurn;
|
|
47
48
|
const fs = __importStar(require("fs"));
|
|
48
49
|
const path = __importStar(require("path"));
|
|
@@ -68,6 +69,8 @@ const bluebubbles_1 = require("./bluebubbles");
|
|
|
68
69
|
const obligations_2 = require("../heart/obligations");
|
|
69
70
|
const contextual_heartbeat_1 = require("./contextual-heartbeat");
|
|
70
71
|
const journal_index_1 = require("../mind/journal-index");
|
|
72
|
+
const habit_parser_1 = require("../heart/daemon/habit-parser");
|
|
73
|
+
const cadence_1 = require("../heart/daemon/cadence");
|
|
71
74
|
const DEFAULT_INNER_DIALOG_INSTINCTS = [
|
|
72
75
|
{
|
|
73
76
|
id: "heartbeat_checkin",
|
|
@@ -127,9 +130,10 @@ function buildInstinctUserMessage(instincts, _reason, state) {
|
|
|
127
130
|
return lines.join("\n");
|
|
128
131
|
}
|
|
129
132
|
function readTaskFile(agentRoot, taskId) {
|
|
130
|
-
// Task files live in collection subdirectories (one-shots, ongoing
|
|
133
|
+
// Task files live in collection subdirectories (one-shots, ongoing).
|
|
131
134
|
// Try each collection, then fall back to root tasks/ for legacy layout.
|
|
132
|
-
|
|
135
|
+
// Habits are no longer in tasks/ — they live at bundle root habits/.
|
|
136
|
+
const collections = ["one-shots", "ongoing", ""];
|
|
133
137
|
for (const collection of collections) {
|
|
134
138
|
try {
|
|
135
139
|
return fs.readFileSync(path.join(agentRoot, "tasks", collection, `${taskId}.md`), "utf8").trim();
|
|
@@ -460,9 +464,57 @@ function createNoOpFriendStore() {
|
|
|
460
464
|
findByExternalId: async () => null,
|
|
461
465
|
};
|
|
462
466
|
}
|
|
467
|
+
function buildParseErrorNudge(parseErrors) {
|
|
468
|
+
if (parseErrors.length === 0)
|
|
469
|
+
return "";
|
|
470
|
+
const lines = parseErrors.map((e) => `I noticed my habit file \`${e.file}\` has invalid frontmatter — I should fix it. (${e.error})`);
|
|
471
|
+
return lines.join("\n");
|
|
472
|
+
}
|
|
473
|
+
function buildAlsoDueLine(agentRoot, currentHabitName, now) {
|
|
474
|
+
const habitsDir = path.join(agentRoot, "habits");
|
|
475
|
+
let files;
|
|
476
|
+
try {
|
|
477
|
+
files = fs.readdirSync(habitsDir);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
const nowMs = now().getTime();
|
|
483
|
+
const alsoDue = [];
|
|
484
|
+
for (const file of files) {
|
|
485
|
+
if (!file.endsWith(".md"))
|
|
486
|
+
continue;
|
|
487
|
+
const stem = file.replace(/\.md$/, "");
|
|
488
|
+
if (stem === currentHabitName)
|
|
489
|
+
continue;
|
|
490
|
+
try {
|
|
491
|
+
const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
|
|
492
|
+
const habit = (0, habit_parser_1.parseHabitFile)(content, path.join(habitsDir, file));
|
|
493
|
+
if (habit.status !== "active" || !habit.cadence)
|
|
494
|
+
continue;
|
|
495
|
+
const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
|
|
496
|
+
if (cadenceMs === null)
|
|
497
|
+
continue;
|
|
498
|
+
if (habit.lastRun === null) {
|
|
499
|
+
alsoDue.push(stem);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const lastRunMs = new Date(habit.lastRun).getTime();
|
|
503
|
+
if (nowMs - lastRunMs >= cadenceMs) {
|
|
504
|
+
alsoDue.push(stem);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
// skip unreadable habits
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (alsoDue.length === 0)
|
|
512
|
+
return "";
|
|
513
|
+
return `also due: ${alsoDue.join(", ")}`;
|
|
514
|
+
}
|
|
463
515
|
async function runInnerDialogTurn(options) {
|
|
464
516
|
const now = options?.now ?? (() => new Date());
|
|
465
|
-
const reason = options?.reason ?? "
|
|
517
|
+
const reason = options?.reason ?? "instinct";
|
|
466
518
|
const sessionFilePath = innerDialogSessionPath();
|
|
467
519
|
const agentName = (0, identity_1.getAgentName)();
|
|
468
520
|
writeInnerDialogRuntimeState(sessionFilePath, {
|
|
@@ -500,47 +552,101 @@ async function runInnerDialogTurn(options) {
|
|
|
500
552
|
const taskContent = readTaskFile((0, identity_1.getAgentRoot)(), options.taskId);
|
|
501
553
|
userContent = buildTaskTriggeredMessage(options.taskId, taskContent, state.checkpoint);
|
|
502
554
|
}
|
|
503
|
-
else if (reason === "
|
|
555
|
+
else if (reason === "habit" && options?.habitName) {
|
|
504
556
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
// Read
|
|
508
|
-
let
|
|
557
|
+
const habitName = options.habitName;
|
|
558
|
+
const habitFilePath = path.join(agentRoot, "habits", `${habitName}.md`);
|
|
559
|
+
// Read and parse the habit file
|
|
560
|
+
let habitBody;
|
|
561
|
+
let habitLastRun = null;
|
|
509
562
|
try {
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
|
|
563
|
+
const habitContent = fs.readFileSync(habitFilePath, "utf-8");
|
|
564
|
+
const parsed = (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath);
|
|
565
|
+
habitBody = parsed.body;
|
|
566
|
+
habitLastRun = parsed.lastRun;
|
|
513
567
|
}
|
|
514
568
|
catch {
|
|
515
|
-
//
|
|
569
|
+
// Habit file missing or unreadable
|
|
570
|
+
}
|
|
571
|
+
if (habitName === "heartbeat") {
|
|
572
|
+
// Heartbeat habit: use contextual heartbeat builder with habit body
|
|
573
|
+
const journalDir = path.join(agentRoot, "journal");
|
|
574
|
+
const runtimeStatePath = innerDialogRuntimeStatePath(sessionFilePath);
|
|
575
|
+
let lastCompletedAt;
|
|
576
|
+
try {
|
|
577
|
+
const raw = fs.readFileSync(runtimeStatePath, "utf-8");
|
|
578
|
+
const runtimeState = JSON.parse(raw);
|
|
579
|
+
lastCompletedAt = typeof runtimeState.lastCompletedAt === "string" ? runtimeState.lastCompletedAt : undefined;
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// no runtime state — will get cold start fallback
|
|
583
|
+
}
|
|
584
|
+
const obligations = (0, obligations_1.listActiveObligations)(agentName);
|
|
585
|
+
const nowMs = now().getTime();
|
|
586
|
+
const pendingObligations = obligations.map((o) => ({
|
|
587
|
+
id: o.id,
|
|
588
|
+
content: o.delegatedContent,
|
|
589
|
+
friendName: o.origin.friendId,
|
|
590
|
+
timestamp: o.createdAt,
|
|
591
|
+
staleness: nowMs - o.createdAt,
|
|
592
|
+
}));
|
|
593
|
+
const heartbeatContent = (0, contextual_heartbeat_1.buildContextualHeartbeat)({
|
|
594
|
+
journalDir,
|
|
595
|
+
lastCompletedAt,
|
|
596
|
+
pendingObligations,
|
|
597
|
+
lastSurfaceAt: undefined,
|
|
598
|
+
checkpoint: state.checkpoint,
|
|
599
|
+
now,
|
|
600
|
+
readJournalDir: () => (0, prompt_1.readJournalFiles)(journalDir),
|
|
601
|
+
habitBody,
|
|
602
|
+
});
|
|
603
|
+
// Build also-due line
|
|
604
|
+
const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
|
|
605
|
+
userContent = alsoDue ? `${heartbeatContent}\n\n${alsoDue}` : heartbeatContent;
|
|
606
|
+
// Parse error nudge
|
|
607
|
+
const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
|
|
608
|
+
if (parseErrorNudge)
|
|
609
|
+
userContent = `${userContent}\n\n${parseErrorNudge}`;
|
|
610
|
+
// Piggyback journal embedding indexing (best-effort, fire-and-forget)
|
|
611
|
+
/* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
|
|
612
|
+
void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
|
|
613
|
+
embed: async () => [],
|
|
614
|
+
}).catch(() => {
|
|
615
|
+
// swallowed: indexing failure must never block heartbeat
|
|
616
|
+
});
|
|
617
|
+
/* v8 ignore stop */
|
|
618
|
+
}
|
|
619
|
+
else if (habitBody !== undefined) {
|
|
620
|
+
// Non-heartbeat habit: basic context + body
|
|
621
|
+
const sections = [];
|
|
622
|
+
// Elapsed time since last run
|
|
623
|
+
if (habitLastRun) {
|
|
624
|
+
const lastRunMs = new Date(habitLastRun).getTime();
|
|
625
|
+
const elapsed = now().getTime() - lastRunMs;
|
|
626
|
+
const minutes = Math.floor(elapsed / 60000);
|
|
627
|
+
if (minutes < 60) {
|
|
628
|
+
sections.push(`it's been ${minutes} ${minutes === 1 ? "minute" : "minutes"} since ${habitName} last ran.`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
const hours = Math.floor(minutes / 60);
|
|
632
|
+
sections.push(`it's been ${hours} ${hours === 1 ? "hour" : "hours"} since ${habitName} last ran.`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
sections.push(`## habit: ${habitName}\n${habitBody}`);
|
|
636
|
+
// Also due line
|
|
637
|
+
const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
|
|
638
|
+
if (alsoDue)
|
|
639
|
+
sections.push(alsoDue);
|
|
640
|
+
// Parse error nudge
|
|
641
|
+
const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
|
|
642
|
+
if (parseErrorNudge)
|
|
643
|
+
sections.push(parseErrorNudge);
|
|
644
|
+
userContent = sections.join("\n\n");
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// Habit file not found
|
|
648
|
+
userContent = `habit "${habitName}" could not be read (file not found or unreadable). check habits/${habitName}.md exists.`;
|
|
516
649
|
}
|
|
517
|
-
// Gather pending obligations for display
|
|
518
|
-
const obligations = (0, obligations_1.listActiveObligations)(agentName);
|
|
519
|
-
const nowMs = now().getTime();
|
|
520
|
-
const pendingObligations = obligations.map((o) => ({
|
|
521
|
-
id: o.id,
|
|
522
|
-
content: o.delegatedContent,
|
|
523
|
-
friendName: o.origin.friendId,
|
|
524
|
-
timestamp: o.createdAt,
|
|
525
|
-
staleness: nowMs - o.createdAt,
|
|
526
|
-
}));
|
|
527
|
-
userContent = (0, contextual_heartbeat_1.buildContextualHeartbeat)({
|
|
528
|
-
journalDir,
|
|
529
|
-
lastCompletedAt,
|
|
530
|
-
pendingObligations,
|
|
531
|
-
lastSurfaceAt: undefined, // TODO: wire when surface tracking is added
|
|
532
|
-
checkpoint: state.checkpoint,
|
|
533
|
-
now,
|
|
534
|
-
readJournalDir: () => (0, prompt_1.readJournalFiles)(journalDir),
|
|
535
|
-
});
|
|
536
|
-
// Piggyback journal embedding indexing (best-effort, fire-and-forget)
|
|
537
|
-
/* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
|
|
538
|
-
void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
|
|
539
|
-
embed: async () => [],
|
|
540
|
-
}).catch(() => {
|
|
541
|
-
// swallowed: indexing failure must never block heartbeat
|
|
542
|
-
});
|
|
543
|
-
/* v8 ignore stop */
|
|
544
650
|
}
|
|
545
651
|
else {
|
|
546
652
|
userContent = buildInstinctUserMessage(instincts, reason, state);
|