@ouro.bot/cli 0.1.0-alpha.664 → 0.1.0-alpha.666
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 +12 -0
- package/dist/arc/flight-recorder.js +267 -4
- package/dist/heart/context-loss-sentinel.js +55 -9
- package/dist/heart/core.js +167 -4
- package/dist/heart/cross-chat-delivery.js +3 -2
- package/dist/heart/daemon/cli-exec.js +50 -1
- package/dist/heart/daemon/cli-help.js +1 -1
- package/dist/heart/daemon/cli-parse.js +36 -2
- package/dist/heart/daemon/daemon-entry.js +24 -5
- package/dist/heart/daemon/daemon.js +10 -1
- package/dist/heart/habits/habit-scheduler.js +24 -5
- package/dist/heart/habits/habit-session.js +563 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +2 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +40 -0
- package/dist/heart/mailbox/mailbox-read.js +3 -1
- package/dist/heart/mailbox/readers/runtime-readers.js +56 -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 +14 -0
- package/dist/repertoire/tools-surface.js +11 -0
- package/dist/repertoire/tools.js +7 -0
- package/dist/senses/inner-dialog-worker.js +153 -69
- package/dist/senses/inner-dialog.js +5 -3
- package/dist/senses/pipeline.js +0 -9
- package/dist/senses/surface-tool.js +2 -1
- package/package.json +1 -1
- package/dist/mailbox-ui/assets/index-BZ60na8O.js +0 -61
- package/dist/mailbox-ui/assets/index-DG6Xf5uL.css +0 -1
package/dist/heart/core.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.getModel = getModel;
|
|
|
6
6
|
exports.getProvider = getProvider;
|
|
7
7
|
exports.createSummarize = createSummarize;
|
|
8
8
|
exports.getProviderDisplayLabel = getProviderDisplayLabel;
|
|
9
|
+
exports.createHabitCallbackBuffer = createHabitCallbackBuffer;
|
|
9
10
|
exports.isChatStyleChannel = isChatStyleChannel;
|
|
10
11
|
exports.buildPonderResult = buildPonderResult;
|
|
11
12
|
exports.isExternalStateQuery = isExternalStateQuery;
|
|
@@ -36,6 +37,7 @@ const packets_1 = require("../arc/packets");
|
|
|
36
37
|
const tool_friction_1 = require("./tool-friction");
|
|
37
38
|
const provider_models_1 = require("./provider-models");
|
|
38
39
|
const provider_credentials_1 = require("./provider-credentials");
|
|
40
|
+
const habit_session_1 = require("./habits/habit-session");
|
|
39
41
|
const provider_attempt_1 = require("./provider-attempt");
|
|
40
42
|
const openai_codex_token_1 = require("./providers/openai-codex-token");
|
|
41
43
|
const _providerRuntimes = {
|
|
@@ -185,6 +187,43 @@ function getProviderDisplayLabel(facing = "human") {
|
|
|
185
187
|
};
|
|
186
188
|
return providerLabelBuilders[provider]();
|
|
187
189
|
}
|
|
190
|
+
function createHabitCallbackBuffer(callbacks) {
|
|
191
|
+
const events = [];
|
|
192
|
+
return {
|
|
193
|
+
callbacks: {
|
|
194
|
+
...callbacks,
|
|
195
|
+
onModelStreamStart: () => { events.push({ kind: "stream-start" }); },
|
|
196
|
+
onTextChunk: (text) => { events.push({ kind: "text", text }); },
|
|
197
|
+
onReasoningChunk: (text) => { events.push({ kind: "reasoning", text }); },
|
|
198
|
+
onClearText: () => { events.push({ kind: "clear" }); },
|
|
199
|
+
flushNow: () => { events.push({ kind: "flush" }); },
|
|
200
|
+
},
|
|
201
|
+
async flush() {
|
|
202
|
+
for (const event of events.splice(0)) {
|
|
203
|
+
switch (event.kind) {
|
|
204
|
+
case "stream-start":
|
|
205
|
+
callbacks.onModelStreamStart();
|
|
206
|
+
break;
|
|
207
|
+
case "text":
|
|
208
|
+
callbacks.onTextChunk(event.text);
|
|
209
|
+
break;
|
|
210
|
+
case "reasoning":
|
|
211
|
+
callbacks.onReasoningChunk(event.text);
|
|
212
|
+
break;
|
|
213
|
+
case "clear":
|
|
214
|
+
callbacks.onClearText?.();
|
|
215
|
+
break;
|
|
216
|
+
case "flush":
|
|
217
|
+
await callbacks.flushNow?.();
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
discard() {
|
|
223
|
+
events.splice(0);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
188
227
|
/**
|
|
189
228
|
* Strip <think>...</think> blocks for the violation-detection check at the
|
|
190
229
|
* end of a streaming turn. Used to tell legitimate text-only responses
|
|
@@ -215,6 +254,85 @@ function hasFreshPendingWork(options) {
|
|
|
215
254
|
return pendingMessages.some((message) => typeof message?.content === "string"
|
|
216
255
|
&& message.content.trim().length > 0);
|
|
217
256
|
}
|
|
257
|
+
const HABIT_CONTROL_TOOLS = new Set(["rest", "ponder", "observe"]);
|
|
258
|
+
function habitToolArgs(call) {
|
|
259
|
+
try {
|
|
260
|
+
const parsed = JSON.parse(call.arguments);
|
|
261
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
262
|
+
return { ok: false, reason: `habit tool '${call.name}' arguments must be a JSON object` };
|
|
263
|
+
}
|
|
264
|
+
return { ok: true, args: parsed };
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return { ok: false, reason: `habit tool '${call.name}' has malformed JSON arguments` };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function highRiskExternalMutation(profile) {
|
|
271
|
+
if (profile.risk !== "high")
|
|
272
|
+
return null;
|
|
273
|
+
const mutates = typeof profile.mutates === "string" ? [profile.mutates] : [...profile.mutates];
|
|
274
|
+
return mutates.includes("external_side_effect") ? mutates.join(", ") : null;
|
|
275
|
+
}
|
|
276
|
+
function recordBlockedHabitSurfaceAttempts(habitSession, toolCalls, reason) {
|
|
277
|
+
if (toolCalls.some((call) => call.name !== "send_message" && call.name !== "surface")) {
|
|
278
|
+
habitSession?.recordError?.(`blocked habit tool batch: ${reason}`);
|
|
279
|
+
}
|
|
280
|
+
if (!habitSession?.recordSurfaceAttempt)
|
|
281
|
+
return;
|
|
282
|
+
for (const call of toolCalls) {
|
|
283
|
+
if (call.name !== "send_message" && call.name !== "surface")
|
|
284
|
+
continue;
|
|
285
|
+
const parsed = habitToolArgs(call);
|
|
286
|
+
const args = parsed.ok ? parsed.args : {};
|
|
287
|
+
habitSession.recordSurfaceAttempt({
|
|
288
|
+
recipient: String(args.friendId ?? args.delegationId ?? "unknown"),
|
|
289
|
+
channel: String(args.channel ?? call.name),
|
|
290
|
+
reason: "blocked",
|
|
291
|
+
result: "blocked",
|
|
292
|
+
rawStatus: "blocked",
|
|
293
|
+
error: reason,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigins, activeToolNames) {
|
|
298
|
+
if (!habitSession)
|
|
299
|
+
return null;
|
|
300
|
+
const granted = new Set(habitSession.toolPolicy.grantedTools);
|
|
301
|
+
const denied = new Set(habitSession.toolPolicy.deniedTools);
|
|
302
|
+
for (const call of toolCalls) {
|
|
303
|
+
if (denied.has(call.name))
|
|
304
|
+
return `habit tool '${call.name}' is denied by this habit session`;
|
|
305
|
+
if (!activeToolNames.has(call.name))
|
|
306
|
+
return `habit tool '${call.name}' was not advertised to this model turn`;
|
|
307
|
+
if (HABIT_CONTROL_TOOLS.has(call.name))
|
|
308
|
+
continue;
|
|
309
|
+
if (!granted.has(call.name))
|
|
310
|
+
return `habit tool '${call.name}' was not granted to this habit session`;
|
|
311
|
+
const parsed = habitToolArgs(call);
|
|
312
|
+
if (!parsed.ok)
|
|
313
|
+
return parsed.reason;
|
|
314
|
+
const riskProfile = (0, tools_1.riskProfileForToolName)(call.name, parsed.args);
|
|
315
|
+
if (!riskProfile)
|
|
316
|
+
return `habit tool '${call.name}' does not have a known executable risk profile`;
|
|
317
|
+
const externalMutation = highRiskExternalMutation(riskProfile);
|
|
318
|
+
if (externalMutation && call.name !== "send_message" && call.name !== "surface") {
|
|
319
|
+
return `habit tool '${call.name}' has high-risk executable mutation ${externalMutation}: ${riskProfile.reason}`;
|
|
320
|
+
}
|
|
321
|
+
if (call.name === "send_message" || call.name === "surface") {
|
|
322
|
+
const route = await (0, habit_session_1.resolveHabitReturnRoute)({
|
|
323
|
+
agentRoot: (0, identity_2.getAgentRoot)(),
|
|
324
|
+
envelope: habitSession.permissionEnvelope,
|
|
325
|
+
toolName: call.name,
|
|
326
|
+
args: parsed.args,
|
|
327
|
+
friendStore: habitSession.friendStore,
|
|
328
|
+
delegatedOrigins,
|
|
329
|
+
});
|
|
330
|
+
if (!route.allowed)
|
|
331
|
+
return route.reason;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
218
336
|
/** Chat-style channels expose the `speak` tool — outer human-conversation channels
|
|
219
337
|
* where mid-turn delivery is meaningful. Inner dialog has `ponder`. MCP returns
|
|
220
338
|
* synchronously. Mail is batch. Anything else (unknown channel) treats as non-chat. */
|
|
@@ -744,6 +862,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
744
862
|
// Augment tool context with reasoning effort controls from provider
|
|
745
863
|
const baseToolContext = options?.toolContext
|
|
746
864
|
?? (options?.orientationFrame ? { signin: async () => undefined, orientationFrame: options.orientationFrame } : undefined);
|
|
865
|
+
const habitSession = options?.habitSession ?? baseToolContext?.habitSession;
|
|
747
866
|
const augmentedToolContext = baseToolContext
|
|
748
867
|
? {
|
|
749
868
|
...baseToolContext,
|
|
@@ -751,8 +870,16 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
751
870
|
setReasoningEffort: (level) => { currentReasoningEffort = level; },
|
|
752
871
|
activeWorkFrame: options?.activeWorkFrame,
|
|
753
872
|
orientationFrame: options?.orientationFrame ?? baseToolContext.orientationFrame,
|
|
873
|
+
...(habitSession ? { habitSession } : {}),
|
|
754
874
|
}
|
|
755
|
-
:
|
|
875
|
+
: habitSession
|
|
876
|
+
? {
|
|
877
|
+
signin: async () => undefined,
|
|
878
|
+
habitSession,
|
|
879
|
+
supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
|
|
880
|
+
setReasoningEffort: (level) => { currentReasoningEffort = level; },
|
|
881
|
+
}
|
|
882
|
+
: undefined;
|
|
756
883
|
// Rebase provider-owned turn state from canonical messages at user-turn start.
|
|
757
884
|
// This prevents stale provider caches from replaying prior-turn context.
|
|
758
885
|
providerRuntime.resetTurnState(messages);
|
|
@@ -766,17 +893,25 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
766
893
|
// Inner dialog gets restTool instead of settleTool (rest = end turn, gated by attention queue).
|
|
767
894
|
// toolChoiceRequired only controls whether tool_choice: "required" is set in the API call.
|
|
768
895
|
const isInnerDialog = channel === "inner";
|
|
896
|
+
const innerHabitCanSendMessage = isInnerDialog
|
|
897
|
+
&& habitSession?.toolPolicy.outwardMessagingAllowed === true
|
|
898
|
+
&& habitSession.toolPolicy.grantedTools.includes("send_message");
|
|
899
|
+
const innerHabitCanSurface = isInnerDialog
|
|
900
|
+
&& (!habitSession || (habitSession.toolPolicy.outwardMessagingAllowed === true
|
|
901
|
+
&& habitSession.toolPolicy.grantedTools.includes("surface")));
|
|
769
902
|
const filteredBaseTools = isInnerDialog
|
|
770
|
-
? baseTools.filter((t) => t.function.name !== "send_message")
|
|
903
|
+
? baseTools.filter((t) => innerHabitCanSendMessage || t.function.name !== "send_message")
|
|
771
904
|
: baseTools;
|
|
772
905
|
const activeTools = [
|
|
773
906
|
...filteredBaseTools,
|
|
774
907
|
tools_1.ponderTool,
|
|
775
|
-
...(isInnerDialog ? [tools_2.surfaceToolDef
|
|
908
|
+
...(isInnerDialog && innerHabitCanSurface ? [tools_2.surfaceToolDef] : []),
|
|
909
|
+
...(isInnerDialog ? [tools_1.restTool] : []),
|
|
776
910
|
...(!isInnerDialog ? [tools_1.observeTool] : []),
|
|
777
911
|
...(!isInnerDialog ? [tools_1.settleTool] : []),
|
|
778
912
|
...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
|
|
779
913
|
];
|
|
914
|
+
const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
|
|
780
915
|
const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
|
|
781
916
|
if (steeringFollowUps.length > 0) {
|
|
782
917
|
const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
|
|
@@ -803,13 +938,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
803
938
|
break;
|
|
804
939
|
}
|
|
805
940
|
try {
|
|
941
|
+
const habitCallbackBufferRef = { current: null };
|
|
806
942
|
const callProviderTurn = async () => {
|
|
807
943
|
callbacks.onModelStart();
|
|
944
|
+
habitCallbackBufferRef.current = habitSession ? createHabitCallbackBuffer(callbacks) : null;
|
|
808
945
|
try {
|
|
809
946
|
return await providerRuntime.streamTurn({
|
|
810
947
|
messages,
|
|
811
948
|
activeTools,
|
|
812
|
-
callbacks,
|
|
949
|
+
callbacks: habitCallbackBufferRef.current?.callbacks ?? callbacks,
|
|
813
950
|
signal,
|
|
814
951
|
traceId,
|
|
815
952
|
toolChoiceRequired,
|
|
@@ -819,6 +956,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
819
956
|
});
|
|
820
957
|
}
|
|
821
958
|
catch (error) {
|
|
959
|
+
habitCallbackBufferRef.current?.discard();
|
|
960
|
+
habitCallbackBufferRef.current = null;
|
|
822
961
|
if (signal?.aborted)
|
|
823
962
|
throw new provider_attempt_1.ProviderAttemptAbortError();
|
|
824
963
|
throw error;
|
|
@@ -890,6 +1029,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
890
1029
|
continue;
|
|
891
1030
|
}
|
|
892
1031
|
const result = attempt.value;
|
|
1032
|
+
const streamCallbackBuffer = habitCallbackBufferRef.current;
|
|
1033
|
+
habitCallbackBufferRef.current = null;
|
|
893
1034
|
// Track usage from the latest API call
|
|
894
1035
|
if (result.usage)
|
|
895
1036
|
lastUsage = result.usage;
|
|
@@ -968,6 +1109,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
968
1109
|
})
|
|
969
1110
|
: null;
|
|
970
1111
|
if (!result.toolCalls.length) {
|
|
1112
|
+
await streamCallbackBuffer?.flush();
|
|
971
1113
|
if (privateReturnTextAckRetryError) {
|
|
972
1114
|
callbacks.onClearText?.();
|
|
973
1115
|
if (noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
|
|
@@ -1050,6 +1192,26 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1050
1192
|
else {
|
|
1051
1193
|
// Reset the retry counter on any successful tool call.
|
|
1052
1194
|
noToolCallRetries = 0;
|
|
1195
|
+
const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins, activeToolNames);
|
|
1196
|
+
if (habitBlockReason) {
|
|
1197
|
+
streamCallbackBuffer?.discard();
|
|
1198
|
+
recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
|
|
1199
|
+
messages.push(msg);
|
|
1200
|
+
const blockedOutput = `blocked: ${habitBlockReason}. No tool side effects from this assistant message were executed.`;
|
|
1201
|
+
for (const call of result.toolCalls) {
|
|
1202
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: blockedOutput });
|
|
1203
|
+
providerRuntime.appendToolOutput(call.id, blockedOutput);
|
|
1204
|
+
}
|
|
1205
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1206
|
+
level: "warn",
|
|
1207
|
+
component: "engine",
|
|
1208
|
+
event: "engine.habit_tool_batch_blocked",
|
|
1209
|
+
message: "habit tool batch blocked before side effects",
|
|
1210
|
+
meta: { reason: habitBlockReason, toolCalls: result.toolCalls.map((call) => call.name) },
|
|
1211
|
+
});
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
await streamCallbackBuffer?.flush();
|
|
1053
1215
|
// Check for settle sole call: intercept before tool execution
|
|
1054
1216
|
if (isSoleSettle) {
|
|
1055
1217
|
/* v8 ignore next -- defensive: JSON.parse catch for malformed settle args @preserve */
|
|
@@ -1501,6 +1663,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1501
1663
|
catch (e) {
|
|
1502
1664
|
toolResult = `error: ${e}`;
|
|
1503
1665
|
success = false;
|
|
1666
|
+
augmentedToolContext?.habitSession?.recordError?.(toolResult);
|
|
1504
1667
|
}
|
|
1505
1668
|
toolResult = (0, tool_friction_1.rewriteToolResultForModel)(tc.name, toolResult, toolFrictionLedger);
|
|
1506
1669
|
(0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);
|
|
@@ -13,11 +13,12 @@ function buildPendingEnvelope(request, agentName, now) {
|
|
|
13
13
|
timestamp: now,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
function queueForLater(request, deps, detail) {
|
|
16
|
+
function queueForLater(request, deps, detail, rawStatus = "queued_for_later") {
|
|
17
17
|
deps.queuePending(buildPendingEnvelope(request, deps.agentName, (deps.now ?? Date.now)()));
|
|
18
18
|
return {
|
|
19
19
|
status: "queued_for_later",
|
|
20
20
|
detail,
|
|
21
|
+
...(rawStatus !== "queued_for_later" ? { rawStatus } : {}),
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
function isExplicitlyAuthorized(request) {
|
|
@@ -94,7 +95,7 @@ async function deliverCrossChatMessage(request, deps) {
|
|
|
94
95
|
});
|
|
95
96
|
return result;
|
|
96
97
|
}
|
|
97
|
-
const result = queueForLater(request, deps, direct.detail.trim() || "live delivery unavailable right now; queued for the next active turn");
|
|
98
|
+
const result = queueForLater(request, deps, direct.detail.trim() || "live delivery unavailable right now; queued for the next active turn", direct.status);
|
|
98
99
|
(0, runtime_1.emitNervesEvent)({
|
|
99
100
|
component: "engine",
|
|
100
101
|
event: "engine.cross_chat_delivery_end",
|
|
@@ -364,6 +364,8 @@ function agentResolutionFailureMode(command) {
|
|
|
364
364
|
case "friend.update":
|
|
365
365
|
case "habit.list":
|
|
366
366
|
case "habit.create":
|
|
367
|
+
case "habit.runs":
|
|
368
|
+
case "habit.inspect":
|
|
367
369
|
case "thoughts":
|
|
368
370
|
case "attention.list":
|
|
369
371
|
case "attention.show":
|
|
@@ -6841,9 +6843,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
6841
6843
|
return result.summary;
|
|
6842
6844
|
}
|
|
6843
6845
|
// ── habit subcommands (local, no daemon socket needed) ──
|
|
6844
|
-
if (command.kind === "habit.list" || command.kind === "habit.create") {
|
|
6846
|
+
if (command.kind === "habit.list" || command.kind === "habit.create" || command.kind === "habit.runs" || command.kind === "habit.inspect") {
|
|
6845
6847
|
const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
|
|
6846
6848
|
const { applyHabitRuntimeState } = await Promise.resolve().then(() => __importStar(require("../habits/habit-runtime-state")));
|
|
6849
|
+
const { listHabitRunReceipts, readHabitRunReceipt } = await Promise.resolve().then(() => __importStar(require("../../arc/flight-recorder")));
|
|
6847
6850
|
/* v8 ignore start -- production default: uses real bundle root @preserve */
|
|
6848
6851
|
const bundleRoot = deps.agentBundleRoot ?? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
|
|
6849
6852
|
/* v8 ignore stop */
|
|
@@ -6874,6 +6877,52 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
6874
6877
|
deps.writeStdout(message);
|
|
6875
6878
|
return message;
|
|
6876
6879
|
}
|
|
6880
|
+
if (command.kind === "habit.runs") {
|
|
6881
|
+
const receipts = listHabitRunReceipts(bundleRoot, { limit: command.limit });
|
|
6882
|
+
const message = receipts.length === 0
|
|
6883
|
+
? "no habit runs found"
|
|
6884
|
+
: receipts.map((receipt) => [
|
|
6885
|
+
receipt.runId,
|
|
6886
|
+
`habit=${receipt.habitName}`,
|
|
6887
|
+
`trigger=${receipt.trigger}`,
|
|
6888
|
+
`outcome=${receipt.outcome}`,
|
|
6889
|
+
`endedAt=${receipt.endedAt}`,
|
|
6890
|
+
`receipt=${receipt.receiptLocator}`,
|
|
6891
|
+
].join(" ")).join("\n");
|
|
6892
|
+
deps.writeStdout(message);
|
|
6893
|
+
(0, runtime_1.emitNervesEvent)({
|
|
6894
|
+
component: "daemon",
|
|
6895
|
+
event: "daemon.habit_runs_cli_read",
|
|
6896
|
+
message: "habit run receipts listed from CLI",
|
|
6897
|
+
meta: { agent: command.agent, limit: command.limit, count: receipts.length },
|
|
6898
|
+
});
|
|
6899
|
+
return message;
|
|
6900
|
+
}
|
|
6901
|
+
if (command.kind === "habit.inspect") {
|
|
6902
|
+
const receipt = readHabitRunReceipt(bundleRoot, command.runId);
|
|
6903
|
+
if (!receipt) {
|
|
6904
|
+
const message = `error: habit run '${command.runId}' not found`;
|
|
6905
|
+
deps.writeStdout(message);
|
|
6906
|
+
deps.setExitCode?.(1);
|
|
6907
|
+
(0, runtime_1.emitNervesEvent)({
|
|
6908
|
+
level: "warn",
|
|
6909
|
+
component: "daemon",
|
|
6910
|
+
event: "daemon.habit_run_cli_read_missing",
|
|
6911
|
+
message: "habit run receipt not found from CLI",
|
|
6912
|
+
meta: { agent: command.agent, runId: command.runId },
|
|
6913
|
+
});
|
|
6914
|
+
return message;
|
|
6915
|
+
}
|
|
6916
|
+
const message = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
6917
|
+
deps.writeStdout(message);
|
|
6918
|
+
(0, runtime_1.emitNervesEvent)({
|
|
6919
|
+
component: "daemon",
|
|
6920
|
+
event: "daemon.habit_run_cli_read",
|
|
6921
|
+
message: "habit run receipt read from CLI",
|
|
6922
|
+
meta: { agent: command.agent, runId: command.runId },
|
|
6923
|
+
});
|
|
6924
|
+
return message;
|
|
6925
|
+
}
|
|
6877
6926
|
// habit.create
|
|
6878
6927
|
const filePath = path.join(habitsDir, `${command.name}.md`);
|
|
6879
6928
|
if (fs.existsSync(filePath)) {
|
|
@@ -127,7 +127,7 @@ exports.COMMAND_REGISTRY = {
|
|
|
127
127
|
poke: {
|
|
128
128
|
category: "Tasks",
|
|
129
129
|
description: "Poke an agent about a task or habit",
|
|
130
|
-
usage: "ouro poke <agent> --task <task-id> | --habit <name>",
|
|
130
|
+
usage: "ouro poke <agent> --task <task-id> | --habit <name> [--trigger poke|launchd|cron|overdue|manual]",
|
|
131
131
|
example: "ouro poke ouroboros --task abc123",
|
|
132
132
|
},
|
|
133
133
|
habit: {
|
|
@@ -17,6 +17,7 @@ exports.parseOuroCommand = parseOuroCommand;
|
|
|
17
17
|
const types_1 = require("../../mind/friends/types");
|
|
18
18
|
const cli_help_1 = require("./cli-help");
|
|
19
19
|
const cli_desk_1 = require("./cli-desk");
|
|
20
|
+
const flight_recorder_1 = require("../../arc/flight-recorder");
|
|
20
21
|
const vault_items_1 = require("./vault-items");
|
|
21
22
|
// ── Shared helpers ──
|
|
22
23
|
function extractAgentFlag(args) {
|
|
@@ -104,9 +105,11 @@ function usage() {
|
|
|
104
105
|
" ouro chat <agent>",
|
|
105
106
|
" ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
|
|
106
107
|
" ouro poke <agent> --task <task-id>",
|
|
107
|
-
" ouro poke <agent> --habit <name>",
|
|
108
|
+
" ouro poke <agent> --habit <name> [--trigger poke|launchd|cron|overdue|manual]",
|
|
108
109
|
" ouro habit list [--agent <name>]",
|
|
109
110
|
" ouro habit create [--agent <name>] <name> [--cadence <interval>]",
|
|
111
|
+
" ouro habit runs [--agent <name>] [--limit <n>]",
|
|
112
|
+
" ouro habit inspect [--agent <name>] <runId>",
|
|
110
113
|
" ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
|
|
111
114
|
" ouro bluebubbles replay [--agent <name>] --message-guid <guid> [--event-type new-message|updated-message] [--json]",
|
|
112
115
|
" ouro friend list [--agent <name>]",
|
|
@@ -175,6 +178,7 @@ function parsePokeCommand(args) {
|
|
|
175
178
|
let taskId;
|
|
176
179
|
let habitName;
|
|
177
180
|
let awaitName;
|
|
181
|
+
let trigger;
|
|
178
182
|
for (let i = 1; i < args.length; i += 1) {
|
|
179
183
|
if (args[i] === "--task") {
|
|
180
184
|
taskId = args[i + 1];
|
|
@@ -188,12 +192,21 @@ function parsePokeCommand(args) {
|
|
|
188
192
|
awaitName = args[i + 1];
|
|
189
193
|
i += 1;
|
|
190
194
|
}
|
|
195
|
+
if (args[i] === "--trigger") {
|
|
196
|
+
const rawTrigger = args[i + 1];
|
|
197
|
+
if (!(0, flight_recorder_1.isHabitRunTrigger)(rawTrigger))
|
|
198
|
+
throw new Error("invalid habit trigger");
|
|
199
|
+
trigger = rawTrigger;
|
|
200
|
+
i += 1;
|
|
201
|
+
}
|
|
191
202
|
}
|
|
192
203
|
// Priority order: --await > --habit > --task
|
|
193
204
|
if (awaitName)
|
|
194
205
|
return { kind: "await.poke", agent, awaitName };
|
|
195
206
|
if (habitName)
|
|
196
|
-
return { kind: "habit.poke", agent, habitName };
|
|
207
|
+
return { kind: "habit.poke", agent, habitName, trigger: trigger ?? "poke" };
|
|
208
|
+
if (trigger)
|
|
209
|
+
throw new Error(`Usage\n${usage()}`);
|
|
197
210
|
if (!taskId)
|
|
198
211
|
throw new Error(`Usage\n${usage()}`);
|
|
199
212
|
return { kind: "task.poke", agent, taskId };
|
|
@@ -227,6 +240,27 @@ function parseHabitCommand(args) {
|
|
|
227
240
|
throw new Error(`Usage\n${usage()}`);
|
|
228
241
|
return { kind: "habit.create", name, ...(agent ? { agent } : {}), ...(cadence ? { cadence } : {}) };
|
|
229
242
|
}
|
|
243
|
+
if (sub === "runs") {
|
|
244
|
+
let limit = 20;
|
|
245
|
+
const options = rest.slice(1);
|
|
246
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
247
|
+
if (options[i] !== "--limit" || !options[i + 1])
|
|
248
|
+
throw new Error(`Usage\n${usage()}`);
|
|
249
|
+
const parsedLimit = Number.parseInt(options[i + 1], 10);
|
|
250
|
+
if (!Number.isInteger(parsedLimit) || String(parsedLimit) !== options[i + 1] || parsedLimit < 1 || parsedLimit > 100) {
|
|
251
|
+
throw new Error("--limit must be an integer between 1 and 100");
|
|
252
|
+
}
|
|
253
|
+
limit = parsedLimit;
|
|
254
|
+
i += 1;
|
|
255
|
+
}
|
|
256
|
+
return { kind: "habit.runs", ...(agent ? { agent } : {}), limit };
|
|
257
|
+
}
|
|
258
|
+
if (sub === "inspect") {
|
|
259
|
+
const positional = rest.slice(1);
|
|
260
|
+
if (positional.length !== 1 || !positional[0])
|
|
261
|
+
throw new Error(`Usage\n${usage()}`);
|
|
262
|
+
return { kind: "habit.inspect", ...(agent ? { agent } : {}), runId: positional[0] };
|
|
263
|
+
}
|
|
230
264
|
throw new Error(`Usage\n${usage()}`);
|
|
231
265
|
}
|
|
232
266
|
function parseLinkCommand(args, kind = "friend.link") {
|
|
@@ -149,9 +149,30 @@ const processManager = new process_manager_1.DaemonProcessManager({
|
|
|
149
149
|
},
|
|
150
150
|
/* v8 ignore stop */
|
|
151
151
|
});
|
|
152
|
-
const
|
|
152
|
+
const taskScheduler = new task_scheduler_1.TaskDrivenScheduler({
|
|
153
153
|
agents: [...managedAgents],
|
|
154
154
|
});
|
|
155
|
+
const habitSchedulers = [];
|
|
156
|
+
const awaitSchedulers = [];
|
|
157
|
+
const scheduler = {
|
|
158
|
+
listJobs: () => [
|
|
159
|
+
...taskScheduler.listJobs(),
|
|
160
|
+
...habitSchedulers.flatMap((habitScheduler) => habitScheduler.listJobs()),
|
|
161
|
+
],
|
|
162
|
+
triggerJob: (jobId) => taskScheduler.triggerJob(jobId),
|
|
163
|
+
triggerHabitJob: async (jobId) => {
|
|
164
|
+
for (const habitScheduler of habitSchedulers) {
|
|
165
|
+
const result = await habitScheduler.triggerJob(jobId, "cron");
|
|
166
|
+
if (result.ok)
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
return { ok: false, message: `unknown habit job: ${jobId}` };
|
|
170
|
+
},
|
|
171
|
+
start: () => taskScheduler.start(),
|
|
172
|
+
stop: () => taskScheduler.stop(),
|
|
173
|
+
reconcile: () => taskScheduler.reconcile(),
|
|
174
|
+
recordTaskRun: (agent, taskId) => taskScheduler.recordTaskRun(agent, taskId),
|
|
175
|
+
};
|
|
155
176
|
const router = new message_router_1.FileMessageRouter();
|
|
156
177
|
const senseManager = new sense_manager_1.DaemonSenseManager({
|
|
157
178
|
agents: [...managedAgents],
|
|
@@ -201,8 +222,6 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
|
|
|
201
222
|
catch { /* recovery is best-effort */ }
|
|
202
223
|
},
|
|
203
224
|
});
|
|
204
|
-
const habitSchedulers = [];
|
|
205
|
-
const awaitSchedulers = [];
|
|
206
225
|
let entryRuntimeStopping = false;
|
|
207
226
|
let stopCommandExitScheduled = false;
|
|
208
227
|
function stopEntryRuntime() {
|
|
@@ -393,8 +412,8 @@ void daemon.start().then(async () => {
|
|
|
393
412
|
agent,
|
|
394
413
|
habitsDir,
|
|
395
414
|
osCronManager,
|
|
396
|
-
onHabitFire: (habitName) => {
|
|
397
|
-
processManager.sendToAgent(agent, { type: "habit", habitName, trigger
|
|
415
|
+
onHabitFire: (habitName, trigger) => {
|
|
416
|
+
processManager.sendToAgent(agent, { type: "habit", habitName, trigger });
|
|
398
417
|
},
|
|
399
418
|
deps: {
|
|
400
419
|
readdir: (dir) => fs.readdirSync(dir),
|
|
@@ -67,6 +67,7 @@ const mailbox_read_1 = require("../mailbox/mailbox-read");
|
|
|
67
67
|
const mailbox_view_1 = require("../mailbox/mailbox-view");
|
|
68
68
|
const provider_visibility_1 = require("../provider-visibility");
|
|
69
69
|
const socket_client_1 = require("./socket-client");
|
|
70
|
+
const flight_recorder_1 = require("../../arc/flight-recorder");
|
|
70
71
|
const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
|
|
71
72
|
/**
|
|
72
73
|
* Defense-in-depth: detect if we're running under vitest. The pidfile lives
|
|
@@ -1236,6 +1237,10 @@ class OuroDaemon {
|
|
|
1236
1237
|
return { ok: true, summary, data: jobs };
|
|
1237
1238
|
}
|
|
1238
1239
|
case "cron.trigger": {
|
|
1240
|
+
const habitResult = await this.scheduler.triggerHabitJob?.(command.jobId);
|
|
1241
|
+
if (habitResult?.ok) {
|
|
1242
|
+
return { ok: true, message: habitResult.message };
|
|
1243
|
+
}
|
|
1239
1244
|
const result = await this.scheduler.triggerJob(command.jobId);
|
|
1240
1245
|
return { ok: result.ok, message: result.message };
|
|
1241
1246
|
}
|
|
@@ -1302,7 +1307,11 @@ class OuroDaemon {
|
|
|
1302
1307
|
};
|
|
1303
1308
|
}
|
|
1304
1309
|
case "habit.poke": {
|
|
1305
|
-
|
|
1310
|
+
const trigger = command.trigger ?? "poke";
|
|
1311
|
+
if (!(0, flight_recorder_1.isHabitRunTrigger)(trigger)) {
|
|
1312
|
+
return { ok: false, error: `invalid habit trigger: ${String(trigger)}` };
|
|
1313
|
+
}
|
|
1314
|
+
this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName, trigger });
|
|
1306
1315
|
return {
|
|
1307
1316
|
ok: true,
|
|
1308
1317
|
message: `poked habit ${command.habitName} for ${command.agent}`,
|
|
@@ -108,7 +108,7 @@ class HabitScheduler {
|
|
|
108
108
|
message: "firing overdue habit (never run)",
|
|
109
109
|
meta: { habitName: habit.name, agent: this.agent },
|
|
110
110
|
});
|
|
111
|
-
this.onHabitFire(habit.name);
|
|
111
|
+
this.onHabitFire(habit.name, "overdue");
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
114
|
const lastRunMs = new Date(habit.lastRun).getTime();
|
|
@@ -120,7 +120,7 @@ class HabitScheduler {
|
|
|
120
120
|
message: "firing overdue habit",
|
|
121
121
|
meta: { habitName: habit.name, agent: this.agent, elapsedMs: elapsed },
|
|
122
122
|
});
|
|
123
|
-
this.onHabitFire(habit.name);
|
|
123
|
+
this.onHabitFire(habit.name, "overdue");
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -174,6 +174,25 @@ class HabitScheduler {
|
|
|
174
174
|
return null;
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
+
listJobs() {
|
|
178
|
+
return this.buildJobs(this.scanHabits())
|
|
179
|
+
.map((job) => ({ id: job.id, schedule: job.schedule, lastRun: job.lastRun }))
|
|
180
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
181
|
+
}
|
|
182
|
+
async triggerJob(jobId, trigger = "cron") {
|
|
183
|
+
const job = this.buildJobs(this.scanHabits()).find((candidate) => candidate.id === jobId);
|
|
184
|
+
if (!job) {
|
|
185
|
+
return { ok: false, message: `unknown habit job: ${jobId}` };
|
|
186
|
+
}
|
|
187
|
+
this.onHabitFire(job.taskId, trigger);
|
|
188
|
+
(0, runtime_1.emitNervesEvent)({
|
|
189
|
+
component: "daemon",
|
|
190
|
+
event: "daemon.habit_job_triggered",
|
|
191
|
+
message: "habit scheduler job triggered",
|
|
192
|
+
meta: { agent: job.agent, habitName: job.taskId, jobId, trigger },
|
|
193
|
+
});
|
|
194
|
+
return { ok: true, message: `triggered habit ${jobId}` };
|
|
195
|
+
}
|
|
177
196
|
watchForChanges() {
|
|
178
197
|
const watchFn = this.deps.watch;
|
|
179
198
|
if (!watchFn)
|
|
@@ -278,7 +297,7 @@ class HabitScheduler {
|
|
|
278
297
|
const output = this.execForVerify("crontab -l");
|
|
279
298
|
const lines = output.split("\n");
|
|
280
299
|
for (const line of lines) {
|
|
281
|
-
const match = line.match(/ouro poke \S+ --habit (\S+)
|
|
300
|
+
const match = line.match(/ouro poke \S+ --habit (\S+)(?:\s+--trigger\s+\S+)?/);
|
|
282
301
|
if (match) {
|
|
283
302
|
verified.add(match[1]);
|
|
284
303
|
}
|
|
@@ -293,7 +312,7 @@ class HabitScheduler {
|
|
|
293
312
|
createTimerFallback(habitName, cadenceMs) {
|
|
294
313
|
const schedule = () => {
|
|
295
314
|
const timer = setTimeout(() => {
|
|
296
|
-
this.onHabitFire(habitName);
|
|
315
|
+
this.onHabitFire(habitName, "overdue");
|
|
297
316
|
schedule();
|
|
298
317
|
}, cadenceMs);
|
|
299
318
|
this.timerFallbacks.set(habitName, timer);
|
|
@@ -362,7 +381,7 @@ class HabitScheduler {
|
|
|
362
381
|
taskId: habit.name,
|
|
363
382
|
schedule: cronSchedule,
|
|
364
383
|
lastRun: habit.lastRun,
|
|
365
|
-
command: `${this.deps.ouroPath} poke ${this.agent} --habit ${habit.name}`,
|
|
384
|
+
command: `${this.deps.ouroPath} poke ${this.agent} --habit ${habit.name} --trigger launchd`,
|
|
366
385
|
taskPath: path.join(this.habitsDir, `${habit.name}.md`),
|
|
367
386
|
});
|
|
368
387
|
}
|