@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
|
@@ -36,13 +36,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.HEARTBEAT_OK_REST_SUPPRESSION_MS = exports.HABIT_RECURSION_BURST_THRESHOLD = exports.HABIT_RECURSION_BURST_WINDOW_MS = exports.HABIT_RECURSION_MIN_INTERVAL_MS = exports.MAX_CONSECUTIVE_INSTINCT_TURNS = void 0;
|
|
37
37
|
exports.createInnerDialogWorker = createInnerDialogWorker;
|
|
38
38
|
exports.startInnerDialogWorker = startInnerDialogWorker;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
40
41
|
const inner_dialog_1 = require("./inner-dialog");
|
|
41
42
|
const runtime_1 = require("../nerves/runtime");
|
|
42
43
|
const identity_1 = require("../heart/identity");
|
|
43
44
|
const pending_1 = require("../mind/pending");
|
|
45
|
+
const habit_parser_1 = require("../heart/habits/habit-parser");
|
|
44
46
|
const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
|
|
47
|
+
const habit_session_1 = require("../heart/habits/habit-session");
|
|
45
48
|
const flight_recorder_1 = require("../arc/flight-recorder");
|
|
49
|
+
const habit_session_summary_1 = require("../heart/habits/habit-session-summary");
|
|
50
|
+
const store_file_1 = require("../mind/friends/store-file");
|
|
51
|
+
const tools_base_1 = require("../repertoire/tools-base");
|
|
52
|
+
const tools_surface_1 = require("../repertoire/tools-surface");
|
|
53
|
+
const tools_1 = require("../repertoire/tools");
|
|
46
54
|
/**
|
|
47
55
|
* Cap on consecutive `instinct` follow-on turns triggered by `hasPendingWork()`
|
|
48
56
|
* with no externally-queued work in between. Without this cap, a turn that
|
|
@@ -83,62 +91,190 @@ function isHeartbeatOkRestResult(result) {
|
|
|
83
91
|
const maybeResult = result;
|
|
84
92
|
return maybeResult.turnOutcome === "rested" && maybeResult.restStatus === "HEARTBEAT_OK";
|
|
85
93
|
}
|
|
86
|
-
function
|
|
94
|
+
function isRecord(value) {
|
|
95
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
function contentToText(content) {
|
|
98
|
+
if (typeof content === "string")
|
|
99
|
+
return content.trim();
|
|
100
|
+
if (!Array.isArray(content))
|
|
101
|
+
return "";
|
|
102
|
+
return content
|
|
103
|
+
.map((part) => isRecord(part) && typeof part.text === "string" ? part.text : "")
|
|
104
|
+
.filter((text) => text.trim().length > 0)
|
|
105
|
+
.join("\n")
|
|
106
|
+
.trim();
|
|
107
|
+
}
|
|
108
|
+
function resultMessages(result) {
|
|
109
|
+
if (Array.isArray(result))
|
|
110
|
+
return result.flatMap((entry) => resultMessages(entry));
|
|
111
|
+
return isRecord(result) && Array.isArray(result.messages) ? result.messages : [];
|
|
112
|
+
}
|
|
113
|
+
function latestAssistantText(results) {
|
|
114
|
+
for (let resultIndex = results.length - 1; resultIndex >= 0; resultIndex--) {
|
|
115
|
+
const messages = resultMessages(results[resultIndex]);
|
|
116
|
+
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex--) {
|
|
117
|
+
const message = messages[messageIndex];
|
|
118
|
+
if (!isRecord(message) || message.role !== "assistant")
|
|
119
|
+
continue;
|
|
120
|
+
const text = contentToText(message.content);
|
|
121
|
+
if (text.length > 0)
|
|
122
|
+
return text.replace(/^checkpoint\s*:\s*/i, "").trim() || text;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function deriveHabitSummarySnapshot(habitRun) {
|
|
128
|
+
const assistant = latestAssistantText(habitRun.results);
|
|
129
|
+
if (assistant)
|
|
130
|
+
return { summary: assistant, decisions: [], nextLikelyStep: null };
|
|
131
|
+
if (habitRun.errors.length > 0) {
|
|
132
|
+
return {
|
|
133
|
+
summary: `Habit ${habitRun.habit.name} finished with errors: ${habitRun.errors.join("; ")}`,
|
|
134
|
+
decisions: [],
|
|
135
|
+
nextLikelyStep: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const surfaced = habitRun.surfaceAttempts.find((attempt) => attempt.result !== "blocked" && attempt.result !== "failed" && attempt.result !== "unavailable");
|
|
139
|
+
if (surfaced) {
|
|
140
|
+
return {
|
|
141
|
+
summary: `Habit ${habitRun.habit.name} surfaced via ${surfaced.recipient}/${surfaced.channel}.`,
|
|
142
|
+
decisions: [],
|
|
143
|
+
nextLikelyStep: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const produced = habitRun.producedRefs.find((ref) => ref.kind !== "none");
|
|
147
|
+
if (produced) {
|
|
148
|
+
return {
|
|
149
|
+
summary: `Habit ${habitRun.habit.name} produced ${produced.kind}: ${produced.locator}.`,
|
|
150
|
+
decisions: [],
|
|
151
|
+
nextLikelyStep: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (habitRun.results.some(isHeartbeatOkRestResult)) {
|
|
155
|
+
return {
|
|
156
|
+
summary: `Habit ${habitRun.habit.name} rested with HEARTBEAT_OK.`,
|
|
157
|
+
decisions: [],
|
|
158
|
+
nextLikelyStep: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
summary: `Habit ${habitRun.habit.name} completed without additional surfaced output.`,
|
|
163
|
+
decisions: [],
|
|
164
|
+
nextLikelyStep: null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function fallbackHabitFile(habitName) {
|
|
168
|
+
return {
|
|
169
|
+
name: habitName,
|
|
170
|
+
title: habitName,
|
|
171
|
+
cadence: null,
|
|
172
|
+
status: "active",
|
|
173
|
+
lastRun: null,
|
|
174
|
+
created: null,
|
|
175
|
+
tools: [],
|
|
176
|
+
origin: null,
|
|
177
|
+
surface: { family: false, originator: false, extra: [] },
|
|
178
|
+
continuity: { mode: "fresh" },
|
|
179
|
+
body: "",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function readHabitForRun(agentRoot, habitName, errors) {
|
|
183
|
+
const habitPath = path.join(agentRoot, "habits", `${habitName}.md`);
|
|
184
|
+
try {
|
|
185
|
+
return (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
189
|
+
errors.push(`habit file could not be read: ${reason}`);
|
|
190
|
+
(0, runtime_1.emitNervesEvent)({
|
|
191
|
+
level: "warn",
|
|
192
|
+
component: "senses",
|
|
193
|
+
event: "senses.habit_file_read_error",
|
|
194
|
+
message: "habit file could not be read for habit session",
|
|
195
|
+
meta: { habitName, habitPath, reason },
|
|
196
|
+
});
|
|
197
|
+
return fallbackHabitFile(habitName);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function prepareHabitRun(habitName, trigger, startedAt) {
|
|
201
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
202
|
+
const errors = [];
|
|
203
|
+
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, readHabitForRun(agentRoot, habitName, errors));
|
|
204
|
+
const runId = (0, flight_recorder_1.createHabitRunId)(habitName, new Date(startedAt));
|
|
205
|
+
const operationId = habit.continuity.mode === "stateful" ? `habit:${habit.name}` : null;
|
|
206
|
+
const priorSessionSummary = readPriorSessionSummary(agentRoot, operationId);
|
|
207
|
+
const paths = (0, habit_session_1.createHabitSessionPaths)(agentRoot, runId, habit.name);
|
|
208
|
+
const friendStore = new store_file_1.FileFriendStore(path.join(agentRoot, "friends"));
|
|
209
|
+
const permissionEnvelope = await (0, habit_session_1.normalizeHabitPermissionEnvelope)(habit, { agentRoot, friendStore });
|
|
210
|
+
const toolPolicy = (0, habit_session_1.filterHabitToolsForEnvelope)([...tools_base_1.baseToolDefinitions, tools_surface_1.surfaceToolDefinition], habit.tools ?? null, permissionEnvelope, riskProfileForHabitPolicy);
|
|
211
|
+
return {
|
|
212
|
+
agentRoot,
|
|
213
|
+
habit,
|
|
214
|
+
runId,
|
|
215
|
+
operationId,
|
|
216
|
+
trigger,
|
|
217
|
+
startedAt,
|
|
218
|
+
priorSessionSummary,
|
|
219
|
+
paths,
|
|
220
|
+
permissionEnvelope,
|
|
221
|
+
toolPolicy,
|
|
222
|
+
friendStore,
|
|
223
|
+
results: [],
|
|
224
|
+
errors,
|
|
225
|
+
producedRefs: [],
|
|
226
|
+
surfaceAttempts: [],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function riskProfileForHabitPolicy(definition, name) {
|
|
230
|
+
const probeArgs = name === "shell" ? { command: "touch /tmp/habit-policy-probe" } : {};
|
|
231
|
+
return (0, tools_1.riskProfileForTool)(definition, name, probeArgs);
|
|
232
|
+
}
|
|
233
|
+
function readPriorSessionSummary(agentRoot, operationId) {
|
|
234
|
+
if (operationId === null)
|
|
235
|
+
return undefined;
|
|
236
|
+
try {
|
|
237
|
+
const summary = (0, habit_session_summary_1.readHabitSessionSummary)(agentRoot, { operationId, which: "latest" });
|
|
238
|
+
if (!summary)
|
|
239
|
+
return { mode: "stateful", summary: null, sources: {}, warnings: [] };
|
|
240
|
+
return {
|
|
241
|
+
mode: "stateful",
|
|
242
|
+
summary: summary.summary,
|
|
243
|
+
sources: summary.sources,
|
|
244
|
+
warnings: summary.warnings,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return {
|
|
249
|
+
mode: "stateful",
|
|
250
|
+
summary: null,
|
|
251
|
+
sources: {},
|
|
252
|
+
warnings: [`prior summary read failed: ${String(error)}`],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = (pendingDir) => (0, pending_1.hasPendingMessages)(pendingDir ?? (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)())), nowSource = () => Date.now()) {
|
|
87
257
|
let running = false;
|
|
88
258
|
const queue = [];
|
|
89
259
|
const lastFireByHabit = new Map();
|
|
90
260
|
const recentHabitFires = [];
|
|
91
261
|
let heartbeatOkRestedAt = null;
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (toolNames.has("send_message") || toolNames.has("surface")) {
|
|
109
|
-
return { outcome: "surfaced", producedRefs: [{ kind: "surface", locator: "tool:send_message_or_surface" }] };
|
|
110
|
-
}
|
|
111
|
-
if (toolNames.has("diary_write") || toolNames.has("note")) {
|
|
112
|
-
return { outcome: "wrote_record", producedRefs: [{ kind: "desk_record", locator: "desk/_record" }] };
|
|
113
|
-
}
|
|
114
|
-
if ([...toolNames].some((name) => name.startsWith("mcp__desk__"))) {
|
|
115
|
-
return { outcome: "updated_desk", producedRefs: [{ kind: "desk_task", locator: "desk/" }] };
|
|
116
|
-
}
|
|
117
|
-
return { outcome: "no_change", producedRefs: [] };
|
|
118
|
-
}
|
|
119
|
-
function recordHabitCompletion(habitName, startedAt = new Date(nowSource()).toISOString(), endedAt = startedAt, trigger = "overdue", result, errors = []) {
|
|
120
|
-
try {
|
|
121
|
-
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
122
|
-
(0, habit_runtime_state_1.recordHabitRun)(agentRoot, habitName, endedAt, {
|
|
123
|
-
definitionPath: path.join(agentRoot, "habits", `${habitName}.md`),
|
|
124
|
-
});
|
|
125
|
-
const { outcome, producedRefs } = habitOutcomeForTurn(result, errors);
|
|
126
|
-
(0, flight_recorder_1.writeHabitRunReceipt)(agentRoot, {
|
|
127
|
-
schemaVersion: 1,
|
|
128
|
-
runId: (0, flight_recorder_1.createHabitRunId)(habitName, new Date(startedAt)),
|
|
129
|
-
habitName,
|
|
130
|
-
trigger,
|
|
131
|
-
startedAt,
|
|
132
|
-
endedAt,
|
|
133
|
-
outcome,
|
|
134
|
-
producedRefs,
|
|
135
|
-
surfaceAttempts: [],
|
|
136
|
-
errors,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Habit file/state may be unavailable during the turn — skip gracefully
|
|
141
|
-
}
|
|
262
|
+
function recordHabitCompletion(habitRun, endedAt = habitRun.startedAt) {
|
|
263
|
+
(0, habit_session_1.completeHabitRun)({
|
|
264
|
+
agentRoot: habitRun.agentRoot,
|
|
265
|
+
habit: habitRun.habit,
|
|
266
|
+
runId: habitRun.runId,
|
|
267
|
+
trigger: habitRun.trigger,
|
|
268
|
+
startedAt: habitRun.startedAt,
|
|
269
|
+
endedAt,
|
|
270
|
+
operationId: habitRun.operationId,
|
|
271
|
+
permissionEnvelope: habitRun.permissionEnvelope,
|
|
272
|
+
toolPolicy: habitRun.toolPolicy,
|
|
273
|
+
producedRefs: habitRun.producedRefs,
|
|
274
|
+
surfaceAttempts: habitRun.surfaceAttempts,
|
|
275
|
+
errors: habitRun.errors,
|
|
276
|
+
summarySnapshot: deriveHabitSummarySnapshot(habitRun),
|
|
277
|
+
});
|
|
142
278
|
}
|
|
143
279
|
function clearHeartbeatRestShield() {
|
|
144
280
|
heartbeatOkRestedAt = null;
|
|
@@ -154,9 +290,11 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
154
290
|
}
|
|
155
291
|
return true;
|
|
156
292
|
}
|
|
157
|
-
function reuseHeartbeatOkRest(habitName) {
|
|
293
|
+
async function reuseHeartbeatOkRest(habitName) {
|
|
158
294
|
const nowIso = new Date(nowSource()).toISOString();
|
|
159
|
-
|
|
295
|
+
const habitRun = await prepareHabitRun(habitName, "overdue", nowIso);
|
|
296
|
+
habitRun.results.push({ turnOutcome: "rested", restStatus: "HEARTBEAT_OK" });
|
|
297
|
+
recordHabitCompletion(habitRun, nowIso);
|
|
160
298
|
(0, runtime_1.emitNervesEvent)({
|
|
161
299
|
level: "info",
|
|
162
300
|
component: "senses",
|
|
@@ -220,19 +358,61 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
220
358
|
let nextHabitName = habitName;
|
|
221
359
|
let nextAwaitName = awaitName;
|
|
222
360
|
let nextTrigger = trigger;
|
|
361
|
+
let nextHabitRun = null;
|
|
223
362
|
let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
|
|
224
363
|
runLoop: do {
|
|
225
364
|
const currentReason = nextReason;
|
|
226
365
|
const currentHabitName = nextHabitName;
|
|
227
366
|
const currentTrigger = nextTrigger ?? "overdue";
|
|
228
|
-
const
|
|
367
|
+
const currentHabitRun = currentReason === "habit" && currentHabitName
|
|
368
|
+
? nextHabitRun && nextHabitRun.habit.name === currentHabitName
|
|
369
|
+
? nextHabitRun
|
|
370
|
+
: await prepareHabitRun(currentHabitName, currentTrigger, new Date(nowSource()).toISOString())
|
|
371
|
+
: null;
|
|
372
|
+
nextHabitRun = null;
|
|
373
|
+
let currentHabitRunFinalized = false;
|
|
374
|
+
const finalizeCurrentHabitRun = () => {
|
|
375
|
+
if (!currentHabitRun || currentHabitRunFinalized)
|
|
376
|
+
return;
|
|
377
|
+
recordHabitCompletion(currentHabitRun, new Date(nowSource()).toISOString());
|
|
378
|
+
currentHabitRunFinalized = true;
|
|
379
|
+
};
|
|
229
380
|
const turnErrors = [];
|
|
230
381
|
if (!(currentReason === "habit" && currentHabitName === "heartbeat")) {
|
|
231
382
|
clearHeartbeatRestShield();
|
|
232
383
|
}
|
|
233
384
|
let turnResult;
|
|
234
385
|
try {
|
|
235
|
-
|
|
386
|
+
const turnOptions = {
|
|
387
|
+
reason: nextReason,
|
|
388
|
+
taskId: nextTaskId,
|
|
389
|
+
habitName: nextHabitName,
|
|
390
|
+
awaitName: nextAwaitName,
|
|
391
|
+
...(currentHabitRun
|
|
392
|
+
? {
|
|
393
|
+
trigger: currentHabitRun.trigger,
|
|
394
|
+
preparedHabit: {
|
|
395
|
+
runId: currentHabitRun.runId,
|
|
396
|
+
trigger: currentHabitRun.trigger,
|
|
397
|
+
operationId: currentHabitRun.operationId,
|
|
398
|
+
habit: currentHabitRun.habit,
|
|
399
|
+
priorSessionSummary: currentHabitRun.priorSessionSummary,
|
|
400
|
+
},
|
|
401
|
+
habitSession: {
|
|
402
|
+
runId: currentHabitRun.runId,
|
|
403
|
+
sessionPath: currentHabitRun.paths.sessionPath,
|
|
404
|
+
pendingDir: currentHabitRun.paths.pendingDir,
|
|
405
|
+
permissionEnvelope: currentHabitRun.permissionEnvelope,
|
|
406
|
+
toolPolicy: currentHabitRun.toolPolicy,
|
|
407
|
+
friendStore: currentHabitRun.friendStore,
|
|
408
|
+
recordProducedRef: (ref) => { currentHabitRun.producedRefs.push(ref); },
|
|
409
|
+
recordSurfaceAttempt: (attempt) => { currentHabitRun.surfaceAttempts.push(attempt); },
|
|
410
|
+
recordError: (error) => { currentHabitRun.errors.push(error); },
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
: {}),
|
|
414
|
+
};
|
|
415
|
+
turnResult = await runTurn(turnOptions);
|
|
236
416
|
}
|
|
237
417
|
catch (error) {
|
|
238
418
|
clearHeartbeatRestShield();
|
|
@@ -251,21 +431,23 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
251
431
|
if (currentReason === "habit" && currentHabitName === "heartbeat") {
|
|
252
432
|
heartbeatOkRestedAt = isHeartbeatOkRestResult(turnResult) ? nowSource() : null;
|
|
253
433
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
434
|
+
if (currentHabitRun) {
|
|
435
|
+
currentHabitRun.results.push(turnResult);
|
|
436
|
+
currentHabitRun.errors.push(...turnErrors);
|
|
257
437
|
}
|
|
258
438
|
// Drain queue first. Externally-queued work resets the instinct cap
|
|
259
439
|
// because a real outside trigger arrived between turns.
|
|
260
440
|
while (queue.length > 0) {
|
|
261
441
|
const next = queue.shift();
|
|
262
442
|
if (next.reason === "habit" && next.habitName === "heartbeat" && shouldReuseHeartbeatOkRest(next.habitName)) {
|
|
263
|
-
|
|
443
|
+
finalizeCurrentHabitRun();
|
|
444
|
+
await reuseHeartbeatOkRest(next.habitName);
|
|
264
445
|
continue;
|
|
265
446
|
}
|
|
266
447
|
if (!(next.reason === "habit" && next.habitName === "heartbeat")) {
|
|
267
448
|
clearHeartbeatRestShield();
|
|
268
449
|
}
|
|
450
|
+
finalizeCurrentHabitRun();
|
|
269
451
|
nextReason = next.reason;
|
|
270
452
|
nextTaskId = next.taskId;
|
|
271
453
|
nextHabitName = next.habitName;
|
|
@@ -278,7 +460,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
278
460
|
// tool that writes to the inner-dialog pending dir during a turn
|
|
279
461
|
// would cause hasPendingWork() to be true here, producing a
|
|
280
462
|
// self-sustaining "instinct" loop with no external input. Cap it.
|
|
281
|
-
if (hasPendingWork()) {
|
|
463
|
+
if (hasPendingWork(currentHabitRun?.paths.pendingDir)) {
|
|
282
464
|
clearHeartbeatRestShield();
|
|
283
465
|
if (consecutiveInstinctTurns >= exports.MAX_CONSECUTIVE_INSTINCT_TURNS) {
|
|
284
466
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -292,16 +474,30 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
292
474
|
lastReason: nextReason,
|
|
293
475
|
},
|
|
294
476
|
});
|
|
477
|
+
finalizeCurrentHabitRun();
|
|
295
478
|
break;
|
|
296
479
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
480
|
+
if (currentReason === "habit" && currentHabitName && currentHabitRun) {
|
|
481
|
+
consecutiveInstinctTurns += 1;
|
|
482
|
+
nextReason = "habit";
|
|
483
|
+
nextTaskId = undefined;
|
|
484
|
+
nextHabitName = currentHabitName;
|
|
485
|
+
nextAwaitName = undefined;
|
|
486
|
+
nextTrigger = currentTrigger;
|
|
487
|
+
nextHabitRun = currentHabitRun;
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
finalizeCurrentHabitRun();
|
|
491
|
+
consecutiveInstinctTurns += 1;
|
|
492
|
+
nextReason = "instinct";
|
|
493
|
+
nextTaskId = undefined;
|
|
494
|
+
nextHabitName = undefined;
|
|
495
|
+
nextAwaitName = undefined;
|
|
496
|
+
nextTrigger = undefined;
|
|
497
|
+
}
|
|
303
498
|
continue;
|
|
304
499
|
}
|
|
500
|
+
finalizeCurrentHabitRun();
|
|
305
501
|
break;
|
|
306
502
|
} while (true);
|
|
307
503
|
}
|
|
@@ -317,7 +513,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
317
513
|
/* v8 ignore next -- defensive fallback: live habit dispatch always sets habitName @preserve */
|
|
318
514
|
const habitName = maybeMessage.habitName ?? "(unnamed)";
|
|
319
515
|
if (shouldReuseHeartbeatOkRest(habitName)) {
|
|
320
|
-
reuseHeartbeatOkRest(habitName);
|
|
516
|
+
await reuseHeartbeatOkRest(habitName);
|
|
321
517
|
return;
|
|
322
518
|
}
|
|
323
519
|
recordHabitFireForRecursion(habitName);
|
|
@@ -335,7 +531,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
335
531
|
if (maybeMessage.type === "heartbeat") {
|
|
336
532
|
// Backward compatibility: heartbeat -> habit/heartbeat
|
|
337
533
|
if (shouldReuseHeartbeatOkRest("heartbeat")) {
|
|
338
|
-
reuseHeartbeatOkRest("heartbeat");
|
|
534
|
+
await reuseHeartbeatOkRest("heartbeat");
|
|
339
535
|
return;
|
|
340
536
|
}
|
|
341
537
|
recordHabitFireForRecursion("heartbeat");
|
|
@@ -645,7 +645,7 @@ function buildHabitSurfacePolicy(origin, surface) {
|
|
|
645
645
|
async function runInnerDialogTurn(options) {
|
|
646
646
|
const now = options?.now ?? (() => new Date());
|
|
647
647
|
const reason = options?.reason ?? "instinct";
|
|
648
|
-
const sessionFilePath = innerDialogSessionPath();
|
|
648
|
+
const sessionFilePath = options?.habitSession?.sessionPath ?? innerDialogSessionPath();
|
|
649
649
|
const agentName = (0, identity_1.getAgentName)();
|
|
650
650
|
writeInnerDialogRuntimeState(sessionFilePath, {
|
|
651
651
|
status: "running",
|
|
@@ -661,7 +661,7 @@ async function runInnerDialogTurn(options) {
|
|
|
661
661
|
resting: false,
|
|
662
662
|
lastHeartbeatAt: now().toISOString(),
|
|
663
663
|
};
|
|
664
|
-
const pendingDir = (0, pending_1.getInnerDialogPendingDir)(agentName);
|
|
664
|
+
const pendingDir = options?.habitSession?.pendingDir ?? (0, pending_1.getInnerDialogPendingDir)(agentName);
|
|
665
665
|
const shouldUseHeldReturnWake = !options?.taskId && reason !== "habit" && reason !== "await"
|
|
666
666
|
? (0, obligations_1.listActiveReturnObligations)(agentName).length > 0
|
|
667
667
|
: false;
|
|
@@ -669,7 +669,7 @@ async function runInnerDialogTurn(options) {
|
|
|
669
669
|
let userContent;
|
|
670
670
|
let habitTools;
|
|
671
671
|
let habitParsedSuccessfully = false;
|
|
672
|
-
if (existingMessages.length === 0) {
|
|
672
|
+
if (existingMessages.length === 0 && !(reason === "habit" && options?.habitName)) {
|
|
673
673
|
// Fresh session: bootstrap message with non-canonical cleanup nudge
|
|
674
674
|
const aspirations = readAspirations((0, identity_1.getAgentRoot)());
|
|
675
675
|
const nonCanonical = (0, bundle_manifest_1.findNonCanonicalBundlePaths)((0, identity_1.getAgentRoot)());
|
|
@@ -692,24 +692,35 @@ async function runInnerDialogTurn(options) {
|
|
|
692
692
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
693
693
|
const habitName = options.habitName;
|
|
694
694
|
const habitFilePath = path.join(agentRoot, "habits", `${habitName}.md`);
|
|
695
|
+
const preparedHabit = options.preparedHabit?.habit.name === habitName ? options.preparedHabit.habit : null;
|
|
695
696
|
// Read and parse the habit file
|
|
696
697
|
let habitBody;
|
|
697
698
|
let habitTitle = habitName;
|
|
698
699
|
let habitLastRun = null;
|
|
699
700
|
let habitOrigin = null;
|
|
700
701
|
let habitSurface = { family: true, originator: true, extra: [] };
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
habitOrigin = parsed.origin;
|
|
709
|
-
habitSurface = parsed.surface;
|
|
702
|
+
if (preparedHabit) {
|
|
703
|
+
habitBody = preparedHabit.body || undefined;
|
|
704
|
+
habitTitle = preparedHabit.title || habitName;
|
|
705
|
+
habitLastRun = preparedHabit.lastRun;
|
|
706
|
+
habitTools = preparedHabit.tools;
|
|
707
|
+
habitOrigin = preparedHabit.origin;
|
|
708
|
+
habitSurface = preparedHabit.surface;
|
|
710
709
|
}
|
|
711
|
-
|
|
712
|
-
|
|
710
|
+
else {
|
|
711
|
+
try {
|
|
712
|
+
const habitContent = fs.readFileSync(habitFilePath, "utf-8");
|
|
713
|
+
const parsed = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath));
|
|
714
|
+
habitBody = parsed.body || undefined;
|
|
715
|
+
habitTitle = parsed.title || habitName;
|
|
716
|
+
habitLastRun = parsed.lastRun;
|
|
717
|
+
habitTools = parsed.tools;
|
|
718
|
+
habitOrigin = parsed.origin;
|
|
719
|
+
habitSurface = parsed.surface;
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// Habit file missing or unreadable
|
|
723
|
+
}
|
|
713
724
|
}
|
|
714
725
|
// If the habit file couldn't be read at all (no body, no title parsed), error message
|
|
715
726
|
if (habitBody === undefined && habitTitle === habitName) {
|
|
@@ -753,6 +764,7 @@ async function runInnerDialogTurn(options) {
|
|
|
753
764
|
arcResume,
|
|
754
765
|
deskOrientation,
|
|
755
766
|
surfacePolicy,
|
|
767
|
+
priorSessionSummary: options.preparedHabit?.habit.name === habitName ? options.preparedHabit.priorSessionSummary : undefined,
|
|
756
768
|
now,
|
|
757
769
|
});
|
|
758
770
|
}
|
|
@@ -875,7 +887,7 @@ async function runInnerDialogTurn(options) {
|
|
|
875
887
|
runAgent: core_1.runAgent,
|
|
876
888
|
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
877
889
|
const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
|
|
878
|
-
(0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
|
|
890
|
+
return (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
|
|
879
891
|
},
|
|
880
892
|
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
881
893
|
signal: options?.signal,
|
|
@@ -917,7 +929,9 @@ async function runInnerDialogTurn(options) {
|
|
|
917
929
|
toolContext: {
|
|
918
930
|
signin: async () => undefined,
|
|
919
931
|
delegatedOrigins: attentionQueue,
|
|
932
|
+
...(options?.habitSession ? { habitSession: options.habitSession } : {}),
|
|
920
933
|
},
|
|
934
|
+
...(options?.habitSession ? { habitSession: options.habitSession } : {}),
|
|
921
935
|
},
|
|
922
936
|
});
|
|
923
937
|
// Post-turn routeDelegatedCompletion removed: delivery is now inline via surface tool.
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -48,7 +48,6 @@ const continuity_1 = require("./continuity");
|
|
|
48
48
|
const manager_1 = require("../heart/bridges/manager");
|
|
49
49
|
const identity_1 = require("../heart/identity");
|
|
50
50
|
const auth_flow_1 = require("../heart/auth/auth-flow");
|
|
51
|
-
const socket_client_1 = require("../heart/daemon/socket-client");
|
|
52
51
|
const active_work_1 = require("../heart/active-work");
|
|
53
52
|
const delegation_1 = require("../heart/delegation");
|
|
54
53
|
const obligations_1 = require("../arc/obligations");
|
|
@@ -828,7 +827,7 @@ async function handleInboundTurn(input) {
|
|
|
828
827
|
/* v8 ignore next -- defensive: error always set when errored @preserve */
|
|
829
828
|
result.error?.message ?? "unknown error", classification, currentProvider, currentBinding.model, agentName, inventory, {}, { currentLane });
|
|
830
829
|
input.failoverState.pending = failoverContext;
|
|
831
|
-
input.postTurn(sessionMessages, session.sessionPath, result.usage);
|
|
830
|
+
await input.postTurn(sessionMessages, session.sessionPath, result.usage);
|
|
832
831
|
try {
|
|
833
832
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
834
833
|
const postTurnArc = readPostTurnFlightRecorderArcSnapshot(agentRoot);
|
|
@@ -903,7 +902,7 @@ async function handleInboundTurn(input) {
|
|
|
903
902
|
? { lastFriendActivityAt }
|
|
904
903
|
: undefined)
|
|
905
904
|
: (Object.keys(continuingState).length > 0 ? continuingState : undefined);
|
|
906
|
-
input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
|
|
905
|
+
await input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
|
|
907
906
|
try {
|
|
908
907
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
909
908
|
const postTurnArc = readPostTurnFlightRecorderArcSnapshot(agentRoot);
|
|
@@ -949,14 +948,6 @@ async function handleInboundTurn(input) {
|
|
|
949
948
|
friendId: resolvedContext.friend.id,
|
|
950
949
|
},
|
|
951
950
|
});
|
|
952
|
-
// DRY cross-session awareness: notify inner dialog that activity happened on another channel
|
|
953
|
-
// Inner dialog's next checkpoint will include this session's state
|
|
954
|
-
if (input.channel !== "inner") {
|
|
955
|
-
try {
|
|
956
|
-
(0, socket_client_1.requestInnerWake)((0, identity_1.getAgentName)(), existingToolContext?.daemonSocketPath).catch(/* v8 ignore next */ () => { });
|
|
957
|
-
}
|
|
958
|
-
catch { /* getAgentName may fail in test environments */ }
|
|
959
|
-
}
|
|
960
951
|
return {
|
|
961
952
|
resolvedContext,
|
|
962
953
|
gateResult,
|
|
@@ -4,7 +4,7 @@ exports.handleSurface = handleSurface;
|
|
|
4
4
|
const attention_queue_1 = require("./attention-queue");
|
|
5
5
|
const runtime_1 = require("../nerves/runtime");
|
|
6
6
|
async function handleSurface(input) {
|
|
7
|
-
const { content, delegationId, friendId, deliveryHint, queue, routeToFriend, advanceObligation, completePonderPacket, fulfillHeartObligation, } = input;
|
|
7
|
+
const { content, delegationId, friendId, deliveryHint, queue, routeToFriend, advanceObligation, completePonderPacket, fulfillHeartObligation, onRouteResult, } = input;
|
|
8
8
|
// Resolve target friend
|
|
9
9
|
let targetFriendId;
|
|
10
10
|
let queueItem;
|
|
@@ -68,6 +68,7 @@ async function handleSurface(input) {
|
|
|
68
68
|
...(result.detail ? { detail: result.detail } : {}),
|
|
69
69
|
},
|
|
70
70
|
});
|
|
71
|
+
onRouteResult?.({ targetFriendId, queueItem, result });
|
|
71
72
|
// On successful routing with delegationId:
|
|
72
73
|
// 1. Advance obligation to "returned" (disk FIRST — crash safety)
|
|
73
74
|
// 2. Dequeue from process-local queue (AFTER obligation advance)
|