@ouro.bot/cli 0.1.0-alpha.674 → 0.1.0-alpha.675
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/README.md +11 -5
- package/changelog.json +13 -0
- package/dist/arc/evolution.js +13 -10
- package/dist/arc/obligations.js +1 -1
- package/dist/heart/active-work.js +3 -3
- package/dist/heart/agent-entry.js +8 -8
- package/dist/heart/commitments.js +1 -1
- package/dist/heart/core.js +45 -23
- package/dist/heart/daemon/agent-config-check.js +24 -0
- package/dist/heart/daemon/agent-discovery.js +28 -17
- package/dist/heart/daemon/agent-service.js +6 -6
- package/dist/heart/daemon/await-private-wake.js +59 -0
- package/dist/heart/daemon/cli-exec.js +180 -24
- package/dist/heart/daemon/cli-help.js +20 -2
- package/dist/heart/daemon/cli-parse.js +45 -2
- package/dist/heart/daemon/cli-render.js +1 -0
- package/dist/heart/daemon/daemon-entry.js +122 -25
- package/dist/heart/daemon/daemon.js +223 -20
- package/dist/heart/daemon/habit-private-wake.js +56 -0
- package/dist/heart/daemon/inner-status.js +3 -3
- package/dist/heart/daemon/message-router.js +39 -3
- package/dist/heart/daemon/pulse.js +43 -7
- package/dist/heart/daemon/socket-client.js +14 -2
- package/dist/heart/daemon/startup-tui.js +9 -1
- package/dist/heart/daemon/thoughts.js +39 -39
- package/dist/heart/habits/habit-scheduler.js +24 -4
- package/dist/heart/mailbox/mailbox-view.js +1 -1
- package/dist/heart/mailbox/readers/agent-machine.js +7 -7
- package/dist/heart/primitives.js +1 -1
- package/dist/heart/private-runtime/decision-renderer.js +156 -0
- package/dist/heart/private-runtime/index.js +18 -0
- package/dist/heart/private-runtime/ledger.js +218 -0
- package/dist/heart/private-runtime/policy.js +205 -0
- package/dist/heart/private-runtime/types.js +2 -0
- package/dist/heart/progress-story.js +1 -1
- package/dist/heart/session-transcript.js +1 -1
- package/dist/heart/tool-activity-callbacks.js +1 -1
- package/dist/heart/tool-description.js +2 -2
- package/dist/heart/tool-loop.js +2 -2
- package/dist/heart/turn-context.js +5 -5
- package/dist/mailbox-ui/assets/index-J8zDwHmE.js +1 -0
- package/dist/mailbox-ui/index.html +1 -1
- package/dist/mailroom/attention.js +1 -1
- package/dist/mind/context.js +2 -2
- package/dist/mind/pending.js +12 -5
- package/dist/mind/prompt.js +32 -25
- package/dist/repertoire/coding/feedback.js +25 -4
- package/dist/repertoire/tools-awaiting.js +2 -2
- package/dist/repertoire/tools-flow.js +4 -4
- package/dist/repertoire/tools-record.js +1 -1
- package/dist/repertoire/tools-session.js +33 -67
- package/dist/repertoire/tools-surface.js +3 -3
- package/dist/senses/cli.js +2 -2
- package/dist/senses/inner-dialog-worker.js +13 -519
- package/dist/senses/inner-dialog.js +10 -961
- package/dist/senses/mail.js +21 -5
- package/dist/senses/pipeline.js +1 -1
- package/dist/senses/private-runtime-worker.js +578 -0
- package/dist/senses/private-runtime.js +1145 -0
- package/dist/senses/shared-turn.js +1 -1
- package/dist/senses/trust-gate.js +2 -2
- package/package.json +1 -1
- package/dist/mailbox-ui/assets/index-CaTIFDmv.js +0 -1
package/dist/senses/mail.js
CHANGED
|
@@ -162,11 +162,23 @@ function renderImportDiscoveryContent(candidatePaths, descriptors) {
|
|
|
162
162
|
"If this matches an expected mailbox backfill, run `ouro mail import-mbox --discover --owner-email <email> --source hey --agent <agent>` first so Ouro can pick the matching archive or report ambiguity.",
|
|
163
163
|
].join("\n");
|
|
164
164
|
}
|
|
165
|
+
function buildMailImportDiscoveryPrivateWakeOptions(input) {
|
|
166
|
+
return {
|
|
167
|
+
reason: "mail import discovery private attention",
|
|
168
|
+
triggerSource: "mail-import-discovery",
|
|
169
|
+
budgetClass: "interactive",
|
|
170
|
+
idempotencyKey: `mail-import-discovery:${input.agentName}:${input.fingerprint}`,
|
|
171
|
+
originRefs: [
|
|
172
|
+
{ kind: "mail-import-discovery", id: input.fingerprint },
|
|
173
|
+
...input.candidatePaths.map((candidatePath) => ({ kind: "mail-import-candidate", id: candidatePath })),
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
165
177
|
async function scanMailImportDiscoveryAttention(input) {
|
|
166
178
|
const nowMs = input.now?.() ?? Date.now();
|
|
167
179
|
const updatedAt = new Date(nowMs).toISOString();
|
|
168
180
|
const statePath = input.statePath ?? importDiscoveryStatePath(input.agentName);
|
|
169
|
-
const pendingDir = input.pendingDir ?? (0, pending_1.
|
|
181
|
+
const pendingDir = input.pendingDir ?? (0, pending_1.getPrivateRuntimePendingDir)(input.agentName);
|
|
170
182
|
const state = readImportDiscoveryState(statePath, updatedAt);
|
|
171
183
|
const existingOperations = (0, background_operations_1.listBackgroundOperations)({
|
|
172
184
|
agentName: input.agentName,
|
|
@@ -187,7 +199,7 @@ async function scanMailImportDiscoveryAttention(input) {
|
|
|
187
199
|
const candidatePaths = stringArray(discovered?.spec?.candidatePaths);
|
|
188
200
|
const descriptors = candidateDescriptors(discovered?.spec?.candidateDescriptors);
|
|
189
201
|
const shouldQueue = Boolean(discovered && fingerprint && fingerprint !== state.lastNotifiedFingerprint);
|
|
190
|
-
if (shouldQueue) {
|
|
202
|
+
if (shouldQueue && fingerprint) {
|
|
191
203
|
(0, pending_1.queuePendingMessage)(pendingDir, {
|
|
192
204
|
from: "mailroom",
|
|
193
205
|
friendId: "self",
|
|
@@ -197,7 +209,11 @@ async function scanMailImportDiscoveryAttention(input) {
|
|
|
197
209
|
timestamp: nowMs,
|
|
198
210
|
mode: "reflect",
|
|
199
211
|
});
|
|
200
|
-
await (0, socket_client_1.
|
|
212
|
+
await (0, socket_client_1.requestPrivateWake)(input.agentName, undefined, buildMailImportDiscoveryPrivateWakeOptions({
|
|
213
|
+
agentName: input.agentName,
|
|
214
|
+
fingerprint,
|
|
215
|
+
candidatePaths,
|
|
216
|
+
})).catch(() => undefined);
|
|
201
217
|
}
|
|
202
218
|
writeImportDiscoveryState(statePath, {
|
|
203
219
|
schemaVersion: 1,
|
|
@@ -271,7 +287,7 @@ async function startMailSenseApp(options) {
|
|
|
271
287
|
const screener = await (0, attention_1.scanMailScreenerAttention)({
|
|
272
288
|
agentName: options.agentName,
|
|
273
289
|
store: resolved.store,
|
|
274
|
-
pendingDir: (0, pending_1.
|
|
290
|
+
pendingDir: (0, pending_1.getPrivateRuntimePendingDir)(options.agentName),
|
|
275
291
|
statePath: attentionPath,
|
|
276
292
|
now,
|
|
277
293
|
});
|
|
@@ -289,7 +305,7 @@ async function startMailSenseApp(options) {
|
|
|
289
305
|
try {
|
|
290
306
|
const importDiscovery = await scanMailImportDiscoveryAttention({
|
|
291
307
|
agentName: options.agentName,
|
|
292
|
-
pendingDir: (0, pending_1.
|
|
308
|
+
pendingDir: (0, pending_1.getPrivateRuntimePendingDir)(options.agentName),
|
|
293
309
|
statePath: importDiscoveryPath,
|
|
294
310
|
now,
|
|
295
311
|
});
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -679,7 +679,7 @@ async function handleInboundTurn(input) {
|
|
|
679
679
|
// Assemble messages: session messages + pending + inbound user messages
|
|
680
680
|
// NOTE: live world-state checkpoint and pending messages are rendered via buildSystem (system prompt sections)
|
|
681
681
|
const extraPrefixSections = input.onPendingDrained?.(pending) ?? [];
|
|
682
|
-
// extraPrefixSections from onPendingDrained still prepend to user message (e.g.,
|
|
682
|
+
// extraPrefixSections from onPendingDrained still prepend to user message (e.g., private-runtime wakes)
|
|
683
683
|
if (extraPrefixSections.length > 0 && input.messages.length > 0) {
|
|
684
684
|
input.messages[0] = prependTurnSections(input.messages[0], extraPrefixSections);
|
|
685
685
|
}
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.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
|
+
exports.createPrivateRuntimeWorker = createPrivateRuntimeWorker;
|
|
38
|
+
exports.startPrivateRuntimeWorker = startPrivateRuntimeWorker;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const private_runtime_1 = require("./private-runtime");
|
|
42
|
+
const runtime_1 = require("../nerves/runtime");
|
|
43
|
+
const identity_1 = require("../heart/identity");
|
|
44
|
+
const pending_1 = require("../mind/pending");
|
|
45
|
+
const habit_parser_1 = require("../heart/habits/habit-parser");
|
|
46
|
+
const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
|
|
47
|
+
const habit_session_1 = require("../heart/habits/habit-session");
|
|
48
|
+
const flight_recorder_1 = require("../arc/flight-recorder");
|
|
49
|
+
const habit_session_summary_1 = require("../heart/habits/habit-session-summary");
|
|
50
|
+
const friends_1 = require("@ouro.bot/friends");
|
|
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");
|
|
54
|
+
/**
|
|
55
|
+
* Cap on consecutive `instinct` follow-on turns triggered by `hasPendingWork()`
|
|
56
|
+
* with no externally-queued work in between. Without this cap, a turn that
|
|
57
|
+
* writes anything back into the private-runtime pending dir as a side effect of
|
|
58
|
+
* processing (e.g. a surface tool routing a response) puts the worker into
|
|
59
|
+
* a self-sustaining loop where the next turn's drain produces another write,
|
|
60
|
+
* and so on. Real workflows rarely chain more than 2–3 instinct turns; an
|
|
61
|
+
* external trigger (habit, poke, chat) resets the counter so legitimate
|
|
62
|
+
* follow-on work is unaffected.
|
|
63
|
+
*
|
|
64
|
+
* Three feels right: legitimate cascading follow-ups (e.g. processing a
|
|
65
|
+
* batch of delegated returns) get through; a true self-loop caps fast.
|
|
66
|
+
*/
|
|
67
|
+
exports.MAX_CONSECUTIVE_INSTINCT_TURNS = 3;
|
|
68
|
+
/**
|
|
69
|
+
* Habit recursion detector thresholds. The instinct cap above protects
|
|
70
|
+
* against pending-dir self-loops; this protects against the *external*
|
|
71
|
+
* IPC self-loop where heartbeat-shaped messages get re-issued faster
|
|
72
|
+
* than their cadence — e.g. a hook misconfigured to repost on every
|
|
73
|
+
* heartbeat, a daemon retry storm, or a stuck timer firing back-to-back.
|
|
74
|
+
*
|
|
75
|
+
* MIN_INTERVAL_MS — two of the same habit within this window is suspect
|
|
76
|
+
* regardless of cadence (no realistic habit fires every few seconds).
|
|
77
|
+
* BURST_THRESHOLD over BURST_WINDOW_MS catches slower runaways that stay
|
|
78
|
+
* just under MIN_INTERVAL_MS.
|
|
79
|
+
*
|
|
80
|
+
* Detection is observation-only: it emits warn-level nerves events, it
|
|
81
|
+
* does not drop the message. An operator (or follow-up auto-recovery)
|
|
82
|
+
* decides what to do with the signal.
|
|
83
|
+
*/
|
|
84
|
+
exports.HABIT_RECURSION_MIN_INTERVAL_MS = 5_000;
|
|
85
|
+
exports.HABIT_RECURSION_BURST_WINDOW_MS = 60_000;
|
|
86
|
+
exports.HABIT_RECURSION_BURST_THRESHOLD = 5;
|
|
87
|
+
exports.HEARTBEAT_OK_REST_SUPPRESSION_MS = 20 * 60_000;
|
|
88
|
+
function defaultHasPendingWork(pendingDir) {
|
|
89
|
+
if (pendingDir)
|
|
90
|
+
return (0, pending_1.hasPendingMessages)(pendingDir);
|
|
91
|
+
const agentArgIndex = process.argv.indexOf("--agent");
|
|
92
|
+
const agentName = agentArgIndex >= 0 ? process.argv[agentArgIndex + 1] : undefined;
|
|
93
|
+
return agentName ? (0, pending_1.hasPendingMessages)((0, pending_1.getPrivateRuntimePendingDir)(agentName)) : false;
|
|
94
|
+
}
|
|
95
|
+
function isHeartbeatOkRestResult(result) {
|
|
96
|
+
if (!result || typeof result !== "object")
|
|
97
|
+
return false;
|
|
98
|
+
const maybeResult = result;
|
|
99
|
+
return maybeResult.turnOutcome === "rested" && maybeResult.restStatus === "HEARTBEAT_OK";
|
|
100
|
+
}
|
|
101
|
+
function isRecord(value) {
|
|
102
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
function contentToText(content) {
|
|
105
|
+
if (typeof content === "string")
|
|
106
|
+
return content.trim();
|
|
107
|
+
if (!Array.isArray(content))
|
|
108
|
+
return "";
|
|
109
|
+
return content
|
|
110
|
+
.map((part) => isRecord(part) && typeof part.text === "string" ? part.text : "")
|
|
111
|
+
.filter((text) => text.trim().length > 0)
|
|
112
|
+
.join("\n")
|
|
113
|
+
.trim();
|
|
114
|
+
}
|
|
115
|
+
function resultMessages(result) {
|
|
116
|
+
if (Array.isArray(result))
|
|
117
|
+
return result.flatMap((entry) => resultMessages(entry));
|
|
118
|
+
return isRecord(result) && Array.isArray(result.messages) ? result.messages : [];
|
|
119
|
+
}
|
|
120
|
+
function latestAssistantText(results) {
|
|
121
|
+
for (let resultIndex = results.length - 1; resultIndex >= 0; resultIndex--) {
|
|
122
|
+
const messages = resultMessages(results[resultIndex]);
|
|
123
|
+
for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex--) {
|
|
124
|
+
const message = messages[messageIndex];
|
|
125
|
+
if (!isRecord(message) || message.role !== "assistant")
|
|
126
|
+
continue;
|
|
127
|
+
const text = contentToText(message.content);
|
|
128
|
+
if (text.length > 0)
|
|
129
|
+
return text.replace(/^checkpoint\s*:\s*/i, "").trim() || text;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function deriveHabitSummarySnapshot(habitRun) {
|
|
135
|
+
const assistant = latestAssistantText(habitRun.results);
|
|
136
|
+
if (assistant)
|
|
137
|
+
return { summary: assistant, decisions: [], nextLikelyStep: null };
|
|
138
|
+
if (habitRun.errors.length > 0) {
|
|
139
|
+
return {
|
|
140
|
+
summary: `Habit ${habitRun.habit.name} finished with errors: ${habitRun.errors.join("; ")}`,
|
|
141
|
+
decisions: [],
|
|
142
|
+
nextLikelyStep: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const surfaced = habitRun.surfaceAttempts.find((attempt) => attempt.result !== "blocked" && attempt.result !== "failed" && attempt.result !== "unavailable");
|
|
146
|
+
if (surfaced) {
|
|
147
|
+
return {
|
|
148
|
+
summary: `Habit ${habitRun.habit.name} surfaced via ${surfaced.recipient}/${surfaced.channel}.`,
|
|
149
|
+
decisions: [],
|
|
150
|
+
nextLikelyStep: null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const produced = habitRun.producedRefs.find((ref) => ref.kind !== "none");
|
|
154
|
+
if (produced) {
|
|
155
|
+
return {
|
|
156
|
+
summary: `Habit ${habitRun.habit.name} produced ${produced.kind}: ${produced.locator}.`,
|
|
157
|
+
decisions: [],
|
|
158
|
+
nextLikelyStep: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (habitRun.results.some(isHeartbeatOkRestResult)) {
|
|
162
|
+
return {
|
|
163
|
+
summary: `Habit ${habitRun.habit.name} rested with HEARTBEAT_OK.`,
|
|
164
|
+
decisions: [],
|
|
165
|
+
nextLikelyStep: null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
summary: `Habit ${habitRun.habit.name} completed without additional surfaced output.`,
|
|
170
|
+
decisions: [],
|
|
171
|
+
nextLikelyStep: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function fallbackHabitFile(habitName) {
|
|
175
|
+
return {
|
|
176
|
+
name: habitName,
|
|
177
|
+
title: habitName,
|
|
178
|
+
cadence: null,
|
|
179
|
+
status: "active",
|
|
180
|
+
lastRun: null,
|
|
181
|
+
created: null,
|
|
182
|
+
tools: [],
|
|
183
|
+
origin: null,
|
|
184
|
+
surface: { family: false, originator: false, extra: [] },
|
|
185
|
+
continuity: { mode: "fresh" },
|
|
186
|
+
body: "",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function readHabitForRun(agentRoot, habitName, errors) {
|
|
190
|
+
const habitPath = path.join(agentRoot, "habits", `${habitName}.md`);
|
|
191
|
+
try {
|
|
192
|
+
return (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
196
|
+
errors.push(`habit file could not be read: ${reason}`);
|
|
197
|
+
(0, runtime_1.emitNervesEvent)({
|
|
198
|
+
level: "warn",
|
|
199
|
+
component: "senses",
|
|
200
|
+
event: "senses.habit_file_read_error",
|
|
201
|
+
message: "habit file could not be read for habit session",
|
|
202
|
+
meta: { habitName, habitPath, reason },
|
|
203
|
+
});
|
|
204
|
+
return fallbackHabitFile(habitName);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function prepareHabitRun(habitName, trigger, startedAt) {
|
|
208
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
209
|
+
const errors = [];
|
|
210
|
+
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, readHabitForRun(agentRoot, habitName, errors));
|
|
211
|
+
const runId = (0, flight_recorder_1.createHabitRunId)(habitName, new Date(startedAt));
|
|
212
|
+
const operationId = habit.continuity.mode === "stateful" ? `habit:${habit.name}` : null;
|
|
213
|
+
const priorSessionSummary = readPriorSessionSummary(agentRoot, operationId);
|
|
214
|
+
const paths = (0, habit_session_1.createHabitSessionPaths)(agentRoot, runId, habit.name);
|
|
215
|
+
const friendStore = new friends_1.FileFriendStore(path.join(agentRoot, "friends"));
|
|
216
|
+
const permissionEnvelope = await (0, habit_session_1.normalizeHabitPermissionEnvelope)(habit, { agentRoot, friendStore });
|
|
217
|
+
const toolPolicy = (0, habit_session_1.filterHabitToolsForEnvelope)([...tools_base_1.baseToolDefinitions, tools_surface_1.surfaceToolDefinition], habit.tools ?? null, permissionEnvelope, riskProfileForHabitPolicy);
|
|
218
|
+
return {
|
|
219
|
+
agentRoot,
|
|
220
|
+
habit,
|
|
221
|
+
runId,
|
|
222
|
+
operationId,
|
|
223
|
+
trigger,
|
|
224
|
+
startedAt,
|
|
225
|
+
priorSessionSummary,
|
|
226
|
+
paths,
|
|
227
|
+
permissionEnvelope,
|
|
228
|
+
toolPolicy,
|
|
229
|
+
friendStore,
|
|
230
|
+
results: [],
|
|
231
|
+
errors,
|
|
232
|
+
producedRefs: [],
|
|
233
|
+
surfaceAttempts: [],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function riskProfileForHabitPolicy(definition, name) {
|
|
237
|
+
const probeArgs = name === "shell" ? { command: "touch /tmp/habit-policy-probe" } : {};
|
|
238
|
+
return (0, tools_1.riskProfileForTool)(definition, name, probeArgs);
|
|
239
|
+
}
|
|
240
|
+
function readPriorSessionSummary(agentRoot, operationId) {
|
|
241
|
+
if (operationId === null)
|
|
242
|
+
return undefined;
|
|
243
|
+
try {
|
|
244
|
+
const summary = (0, habit_session_summary_1.readHabitSessionSummary)(agentRoot, { operationId, which: "latest" });
|
|
245
|
+
if (!summary)
|
|
246
|
+
return { mode: "stateful", summary: null, sources: {}, warnings: [] };
|
|
247
|
+
return {
|
|
248
|
+
mode: "stateful",
|
|
249
|
+
summary: summary.summary,
|
|
250
|
+
sources: summary.sources,
|
|
251
|
+
warnings: summary.warnings,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return {
|
|
256
|
+
mode: "stateful",
|
|
257
|
+
summary: null,
|
|
258
|
+
sources: {},
|
|
259
|
+
warnings: [`prior summary read failed: ${String(error)}`],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1.runPrivateRuntimeTurn)(options), hasPendingWork = defaultHasPendingWork, nowSource = () => Date.now()) {
|
|
264
|
+
let running = false;
|
|
265
|
+
const queue = [];
|
|
266
|
+
const lastFireByHabit = new Map();
|
|
267
|
+
const recentHabitFires = [];
|
|
268
|
+
let heartbeatOkRestedAt = null;
|
|
269
|
+
function recordHabitCompletion(habitRun, endedAt = habitRun.startedAt) {
|
|
270
|
+
(0, habit_session_1.completeHabitRun)({
|
|
271
|
+
agentRoot: habitRun.agentRoot,
|
|
272
|
+
habit: habitRun.habit,
|
|
273
|
+
runId: habitRun.runId,
|
|
274
|
+
trigger: habitRun.trigger,
|
|
275
|
+
startedAt: habitRun.startedAt,
|
|
276
|
+
endedAt,
|
|
277
|
+
operationId: habitRun.operationId,
|
|
278
|
+
permissionEnvelope: habitRun.permissionEnvelope,
|
|
279
|
+
toolPolicy: habitRun.toolPolicy,
|
|
280
|
+
producedRefs: habitRun.producedRefs,
|
|
281
|
+
surfaceAttempts: habitRun.surfaceAttempts,
|
|
282
|
+
errors: habitRun.errors,
|
|
283
|
+
summarySnapshot: deriveHabitSummarySnapshot(habitRun),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function clearHeartbeatRestShield() {
|
|
287
|
+
heartbeatOkRestedAt = null;
|
|
288
|
+
}
|
|
289
|
+
function shouldReuseHeartbeatOkRest(habitName) {
|
|
290
|
+
if (habitName !== "heartbeat" || heartbeatOkRestedAt === null)
|
|
291
|
+
return false;
|
|
292
|
+
if (nowSource() - heartbeatOkRestedAt > exports.HEARTBEAT_OK_REST_SUPPRESSION_MS)
|
|
293
|
+
return false;
|
|
294
|
+
if (hasPendingWork()) {
|
|
295
|
+
clearHeartbeatRestShield();
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
async function reuseHeartbeatOkRest(habitName) {
|
|
301
|
+
const nowIso = new Date(nowSource()).toISOString();
|
|
302
|
+
const habitRun = await prepareHabitRun(habitName, "overdue", nowIso);
|
|
303
|
+
habitRun.results.push({ turnOutcome: "rested", restStatus: "HEARTBEAT_OK" });
|
|
304
|
+
recordHabitCompletion(habitRun, nowIso);
|
|
305
|
+
(0, runtime_1.emitNervesEvent)({
|
|
306
|
+
level: "info",
|
|
307
|
+
component: "senses",
|
|
308
|
+
event: "senses.heartbeat_ok_rest_reused",
|
|
309
|
+
message: "heartbeat skipped because previous HEARTBEAT_OK rest is still valid and no pending work exists",
|
|
310
|
+
meta: {
|
|
311
|
+
habitName,
|
|
312
|
+
quietWindowMs: exports.HEARTBEAT_OK_REST_SUPPRESSION_MS,
|
|
313
|
+
restedAgoMs: nowSource() - heartbeatOkRestedAt,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function recordHabitFireForRecursion(habitName) {
|
|
318
|
+
const now = nowSource();
|
|
319
|
+
const previous = lastFireByHabit.get(habitName);
|
|
320
|
+
if (previous !== undefined) {
|
|
321
|
+
const intervalMs = now - previous;
|
|
322
|
+
if (intervalMs < exports.HABIT_RECURSION_MIN_INTERVAL_MS) {
|
|
323
|
+
(0, runtime_1.emitNervesEvent)({
|
|
324
|
+
level: "warn",
|
|
325
|
+
component: "senses",
|
|
326
|
+
event: "senses.habit_recursion_suspected",
|
|
327
|
+
message: "habit fired suspiciously fast after the previous fire — possible self-recursion or duplicate dispatch",
|
|
328
|
+
meta: {
|
|
329
|
+
habitName,
|
|
330
|
+
intervalMs,
|
|
331
|
+
thresholdMs: exports.HABIT_RECURSION_MIN_INTERVAL_MS,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
lastFireByHabit.set(habitName, now);
|
|
337
|
+
recentHabitFires.push(now);
|
|
338
|
+
while (recentHabitFires.length > 0 && now - recentHabitFires[0] > exports.HABIT_RECURSION_BURST_WINDOW_MS) {
|
|
339
|
+
recentHabitFires.shift();
|
|
340
|
+
}
|
|
341
|
+
if (recentHabitFires.length >= exports.HABIT_RECURSION_BURST_THRESHOLD) {
|
|
342
|
+
(0, runtime_1.emitNervesEvent)({
|
|
343
|
+
level: "warn",
|
|
344
|
+
component: "senses",
|
|
345
|
+
event: "senses.habit_recursion_burst",
|
|
346
|
+
message: "habit messages arriving in a burst — possible runaway loop",
|
|
347
|
+
meta: {
|
|
348
|
+
count: recentHabitFires.length,
|
|
349
|
+
windowMs: exports.HABIT_RECURSION_BURST_WINDOW_MS,
|
|
350
|
+
thresholdCount: exports.HABIT_RECURSION_BURST_THRESHOLD,
|
|
351
|
+
lastHabitName: habitName,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function run(reason, taskId, habitName, awaitName, trigger, privateTurnDecision) {
|
|
357
|
+
if (running) {
|
|
358
|
+
queue.push({ reason, taskId, habitName, awaitName, trigger, privateTurnDecision });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
running = true;
|
|
362
|
+
try {
|
|
363
|
+
let nextReason = reason;
|
|
364
|
+
let nextTaskId = taskId;
|
|
365
|
+
let nextHabitName = habitName;
|
|
366
|
+
let nextAwaitName = awaitName;
|
|
367
|
+
let nextTrigger = trigger;
|
|
368
|
+
let nextPrivateTurnDecision = privateTurnDecision;
|
|
369
|
+
let nextHabitRun = null;
|
|
370
|
+
let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
|
|
371
|
+
runLoop: do {
|
|
372
|
+
const currentReason = nextReason;
|
|
373
|
+
const currentHabitName = nextHabitName;
|
|
374
|
+
const currentTrigger = nextTrigger ?? "overdue";
|
|
375
|
+
const currentHabitRun = currentReason === "habit" && currentHabitName
|
|
376
|
+
? nextHabitRun && nextHabitRun.habit.name === currentHabitName
|
|
377
|
+
? nextHabitRun
|
|
378
|
+
: await prepareHabitRun(currentHabitName, currentTrigger, new Date(nowSource()).toISOString())
|
|
379
|
+
: null;
|
|
380
|
+
nextHabitRun = null;
|
|
381
|
+
let currentHabitRunFinalized = false;
|
|
382
|
+
const finalizeCurrentHabitRun = () => {
|
|
383
|
+
if (!currentHabitRun || currentHabitRunFinalized)
|
|
384
|
+
return;
|
|
385
|
+
recordHabitCompletion(currentHabitRun, new Date(nowSource()).toISOString());
|
|
386
|
+
currentHabitRunFinalized = true;
|
|
387
|
+
};
|
|
388
|
+
const turnErrors = [];
|
|
389
|
+
if (!(currentReason === "habit" && currentHabitName === "heartbeat")) {
|
|
390
|
+
clearHeartbeatRestShield();
|
|
391
|
+
}
|
|
392
|
+
let turnResult;
|
|
393
|
+
try {
|
|
394
|
+
const turnOptions = {
|
|
395
|
+
reason: nextReason,
|
|
396
|
+
taskId: nextTaskId,
|
|
397
|
+
habitName: nextHabitName,
|
|
398
|
+
awaitName: nextAwaitName,
|
|
399
|
+
...(nextPrivateTurnDecision ? { privateTurnDecision: nextPrivateTurnDecision } : {}),
|
|
400
|
+
...(currentHabitRun
|
|
401
|
+
? {
|
|
402
|
+
trigger: currentHabitRun.trigger,
|
|
403
|
+
preparedHabit: {
|
|
404
|
+
runId: currentHabitRun.runId,
|
|
405
|
+
trigger: currentHabitRun.trigger,
|
|
406
|
+
operationId: currentHabitRun.operationId,
|
|
407
|
+
habit: currentHabitRun.habit,
|
|
408
|
+
priorSessionSummary: currentHabitRun.priorSessionSummary,
|
|
409
|
+
},
|
|
410
|
+
habitSession: {
|
|
411
|
+
runId: currentHabitRun.runId,
|
|
412
|
+
sessionPath: currentHabitRun.paths.sessionPath,
|
|
413
|
+
pendingDir: currentHabitRun.paths.pendingDir,
|
|
414
|
+
permissionEnvelope: currentHabitRun.permissionEnvelope,
|
|
415
|
+
toolPolicy: currentHabitRun.toolPolicy,
|
|
416
|
+
friendStore: currentHabitRun.friendStore,
|
|
417
|
+
recordProducedRef: (ref) => { currentHabitRun.producedRefs.push(ref); },
|
|
418
|
+
recordSurfaceAttempt: (attempt) => { currentHabitRun.surfaceAttempts.push(attempt); },
|
|
419
|
+
recordError: (error) => { currentHabitRun.errors.push(error); },
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
: {}),
|
|
423
|
+
};
|
|
424
|
+
turnResult = await runTurn(turnOptions);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
clearHeartbeatRestShield();
|
|
428
|
+
turnErrors.push(error instanceof Error ? error.message : String(error));
|
|
429
|
+
(0, runtime_1.emitNervesEvent)({
|
|
430
|
+
level: "error",
|
|
431
|
+
component: "senses",
|
|
432
|
+
event: "senses.private_runtime_worker_error",
|
|
433
|
+
message: "private-runtime worker turn failed",
|
|
434
|
+
meta: {
|
|
435
|
+
reason: nextReason,
|
|
436
|
+
error: error instanceof Error ? error.message : String(error),
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (currentReason === "habit" && currentHabitName === "heartbeat") {
|
|
441
|
+
heartbeatOkRestedAt = isHeartbeatOkRestResult(turnResult) ? nowSource() : null;
|
|
442
|
+
}
|
|
443
|
+
if (currentHabitRun) {
|
|
444
|
+
currentHabitRun.results.push(turnResult);
|
|
445
|
+
currentHabitRun.errors.push(...turnErrors);
|
|
446
|
+
}
|
|
447
|
+
// Drain queue first. Externally-queued work resets the instinct cap
|
|
448
|
+
// because a real outside trigger arrived between turns.
|
|
449
|
+
while (queue.length > 0) {
|
|
450
|
+
const next = queue.shift();
|
|
451
|
+
if (next.reason === "habit" && next.habitName === "heartbeat" && shouldReuseHeartbeatOkRest(next.habitName)) {
|
|
452
|
+
finalizeCurrentHabitRun();
|
|
453
|
+
await reuseHeartbeatOkRest(next.habitName);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (!(next.reason === "habit" && next.habitName === "heartbeat")) {
|
|
457
|
+
clearHeartbeatRestShield();
|
|
458
|
+
}
|
|
459
|
+
finalizeCurrentHabitRun();
|
|
460
|
+
nextReason = next.reason;
|
|
461
|
+
nextTaskId = next.taskId;
|
|
462
|
+
nextHabitName = next.habitName;
|
|
463
|
+
nextAwaitName = next.awaitName;
|
|
464
|
+
nextTrigger = next.trigger;
|
|
465
|
+
nextPrivateTurnDecision = next.privateTurnDecision;
|
|
466
|
+
consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
|
|
467
|
+
continue runLoop;
|
|
468
|
+
}
|
|
469
|
+
// Then check hasPendingWork fallback. This is the loop site: any
|
|
470
|
+
// tool that writes to the private-runtime pending dir during a turn
|
|
471
|
+
// would cause hasPendingWork() to be true here, producing a
|
|
472
|
+
// self-sustaining "instinct" loop with no external input. Cap it.
|
|
473
|
+
if (hasPendingWork(currentHabitRun?.paths.pendingDir)) {
|
|
474
|
+
clearHeartbeatRestShield();
|
|
475
|
+
if (consecutiveInstinctTurns >= exports.MAX_CONSECUTIVE_INSTINCT_TURNS) {
|
|
476
|
+
(0, runtime_1.emitNervesEvent)({
|
|
477
|
+
level: "warn",
|
|
478
|
+
component: "senses",
|
|
479
|
+
event: "senses.private_runtime_worker_instinct_loop_capped",
|
|
480
|
+
message: "private-runtime worker stopped chaining instinct turns; pending work remains for next external trigger",
|
|
481
|
+
meta: {
|
|
482
|
+
consecutiveInstinctTurns,
|
|
483
|
+
cap: exports.MAX_CONSECUTIVE_INSTINCT_TURNS,
|
|
484
|
+
lastReason: nextReason,
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
finalizeCurrentHabitRun();
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
if (currentReason === "habit" && currentHabitName && currentHabitRun) {
|
|
491
|
+
consecutiveInstinctTurns += 1;
|
|
492
|
+
nextReason = "habit";
|
|
493
|
+
nextTaskId = undefined;
|
|
494
|
+
nextHabitName = currentHabitName;
|
|
495
|
+
nextAwaitName = undefined;
|
|
496
|
+
nextTrigger = currentTrigger;
|
|
497
|
+
nextPrivateTurnDecision = undefined;
|
|
498
|
+
nextHabitRun = currentHabitRun;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
finalizeCurrentHabitRun();
|
|
502
|
+
consecutiveInstinctTurns += 1;
|
|
503
|
+
nextReason = "instinct";
|
|
504
|
+
nextTaskId = undefined;
|
|
505
|
+
nextHabitName = undefined;
|
|
506
|
+
nextAwaitName = undefined;
|
|
507
|
+
nextTrigger = undefined;
|
|
508
|
+
nextPrivateTurnDecision = undefined;
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
finalizeCurrentHabitRun();
|
|
513
|
+
break;
|
|
514
|
+
} while (true);
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
running = false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function handleMessage(message) {
|
|
521
|
+
if (!message || typeof message !== "object")
|
|
522
|
+
return;
|
|
523
|
+
const maybeMessage = message;
|
|
524
|
+
if (maybeMessage.type === "habit") {
|
|
525
|
+
/* v8 ignore next -- defensive fallback: live habit dispatch always sets habitName @preserve */
|
|
526
|
+
const habitName = maybeMessage.habitName ?? "(unnamed)";
|
|
527
|
+
if (shouldReuseHeartbeatOkRest(habitName)) {
|
|
528
|
+
await reuseHeartbeatOkRest(habitName);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
recordHabitFireForRecursion(habitName);
|
|
532
|
+
await run("habit", undefined, maybeMessage.habitName, undefined, maybeMessage.trigger ?? "overdue", maybeMessage.privateTurnDecision);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (maybeMessage.type === "await") {
|
|
536
|
+
clearHeartbeatRestShield();
|
|
537
|
+
/* v8 ignore next -- defensive fallback: live await dispatch always sets awaitName @preserve */
|
|
538
|
+
const awaitName = maybeMessage.awaitName ?? "(unnamed)";
|
|
539
|
+
recordHabitFireForRecursion(`await:${awaitName}`);
|
|
540
|
+
await run("await", undefined, undefined, maybeMessage.awaitName, undefined, maybeMessage.privateTurnDecision);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (maybeMessage.type === "heartbeat") {
|
|
544
|
+
// Backward compatibility: heartbeat -> habit/heartbeat
|
|
545
|
+
if (shouldReuseHeartbeatOkRest("heartbeat")) {
|
|
546
|
+
await reuseHeartbeatOkRest("heartbeat");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
recordHabitFireForRecursion("heartbeat");
|
|
550
|
+
await run("habit", undefined, "heartbeat", undefined, "overdue", maybeMessage.privateTurnDecision);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (maybeMessage.type === "poke") {
|
|
554
|
+
clearHeartbeatRestShield();
|
|
555
|
+
await run("instinct", maybeMessage.taskId, undefined, undefined, undefined, maybeMessage.privateTurnDecision);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (maybeMessage.type === "chat" ||
|
|
559
|
+
maybeMessage.type === "message") {
|
|
560
|
+
clearHeartbeatRestShield();
|
|
561
|
+
await run("instinct", undefined, undefined, undefined, undefined, maybeMessage.privateTurnDecision);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (maybeMessage.type === "shutdown") {
|
|
565
|
+
process.exit(0);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { run, handleMessage };
|
|
569
|
+
}
|
|
570
|
+
async function startPrivateRuntimeWorker() {
|
|
571
|
+
const worker = createPrivateRuntimeWorker();
|
|
572
|
+
process.on("message", (message) => {
|
|
573
|
+
void worker.handleMessage(message);
|
|
574
|
+
});
|
|
575
|
+
process.on("disconnect", () => {
|
|
576
|
+
process.exit(0);
|
|
577
|
+
});
|
|
578
|
+
}
|