@ouro.bot/cli 0.1.0-alpha.666 → 0.1.0-alpha.668

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.
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createMailboxHttpRequestHandler = createMailboxHttpRequestHandler;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const mailbox_http_hooks_1 = require("./mailbox-http-hooks");
39
40
  const mailbox_http_response_1 = require("./mailbox-http-response");
40
41
  const mailbox_http_static_1 = require("./mailbox-http-static");
41
42
  function decodePathSegment(value) {
@@ -46,6 +47,16 @@ function decodePathSegment(value) {
46
47
  return null;
47
48
  }
48
49
  }
50
+ function rawRequestTargetsUnsafeAgent(urlValue = "/") {
51
+ const rawTarget = urlValue.split(/[?#]/, 1)[0];
52
+ const rawPath = rawTarget.replace(/\/+$/, "") || "/";
53
+ const pathname = (0, mailbox_http_static_1.normalizeLegacyMailboxApiPath)(rawPath);
54
+ const rawAgentMatch = /^\/api\/agents\/([^/]+)(?:\/|$)/.exec(pathname);
55
+ if (!rawAgentMatch)
56
+ return false;
57
+ const agent = decodePathSegment(rawAgentMatch[1]);
58
+ return !agent || !(0, mailbox_http_hooks_1.isSafeMailboxAgentName)(agent);
59
+ }
49
60
  function parseHabitRunLimit(urlValue) {
50
61
  const rawLimit = new URL(urlValue, "http://127.0.0.1").searchParams.get("limit");
51
62
  if (rawLimit === null)
@@ -55,9 +66,47 @@ function parseHabitRunLimit(urlValue) {
55
66
  const limit = Number.parseInt(rawLimit, 10);
56
67
  return limit >= 1 && limit <= 100 ? limit : null;
57
68
  }
69
+ const VALID_HABIT_SUMMARY_WHICH = new Set(["latest", "previous", "latest-success", "latest-failure"]);
70
+ function firstQueryValue(params, names) {
71
+ for (const name of names) {
72
+ const value = params.get(name);
73
+ if (value !== null && value.trim().length > 0)
74
+ return value;
75
+ }
76
+ return undefined;
77
+ }
78
+ function parseHabitSummarySelector(urlValue) {
79
+ const params = new URL(urlValue, "http://127.0.0.1").searchParams;
80
+ const runId = firstQueryValue(params, ["runId", "run-id"]);
81
+ const habitName = firstQueryValue(params, ["habitName", "habit"]);
82
+ const operationId = firstQueryValue(params, ["operationId", "operation-id"]);
83
+ const which = firstQueryValue(params, ["which"]);
84
+ if (runId !== undefined && (habitName !== undefined || operationId !== undefined || which !== undefined)) {
85
+ return { ok: false, error: "--run-id cannot be combined with --habit, --operation-id, or --which" };
86
+ }
87
+ if (runId === undefined && habitName === undefined && operationId === undefined) {
88
+ return { ok: false, error: "provide runId, habitName, or operationId" };
89
+ }
90
+ if (which !== undefined && !VALID_HABIT_SUMMARY_WHICH.has(which)) {
91
+ return { ok: false, error: "which must be latest, previous, latest-success, or latest-failure" };
92
+ }
93
+ return {
94
+ ok: true,
95
+ selector: {
96
+ ...(runId ? { runId } : {}),
97
+ ...(habitName ? { habitName } : {}),
98
+ ...(operationId ? { operationId } : {}),
99
+ ...(which ? { which } : {}),
100
+ },
101
+ };
102
+ }
58
103
  function createMailboxHttpRequestHandler(options) {
59
104
  const staticFiles = options.staticFiles ?? { resolveSpaDistDir: mailbox_http_static_1.resolveSpaDistDir, serveStaticFile: mailbox_http_static_1.serveStaticFile };
60
105
  return (request, response) => {
106
+ if (rawRequestTargetsUnsafeAgent(request.url)) {
107
+ (0, mailbox_http_response_1.writeJson)(response, 400, { ok: false, error: "agent name must be a safe bundle name" });
108
+ return;
109
+ }
61
110
  let pathname = (0, mailbox_http_static_1.normalizeMailboxRequestPath)(request.url);
62
111
  const origin = `http://${options.host}:${options.getPort()}`;
63
112
  if (pathname.startsWith("/assets/")) {
@@ -99,8 +148,14 @@ function createMailboxHttpRequestHandler(options) {
99
148
  }
100
149
  const agentMatch = /^\/api\/agents\/([^/]+)(?:\/(.+))?$/.exec(pathname);
101
150
  if (agentMatch) {
151
+ const agent = decodePathSegment(agentMatch[1]);
152
+ /* v8 ignore next -- rawRequestTargetsUnsafeAgent rejects unsafe/malformed agent segments before normalization @preserve */
153
+ if (!agent || !(0, mailbox_http_hooks_1.isSafeMailboxAgentName)(agent)) {
154
+ (0, mailbox_http_response_1.writeJson)(response, 400, { ok: false, error: "agent name must be a safe bundle name" });
155
+ return;
156
+ }
102
157
  void handleAgentRoute(request, response, {
103
- agent: decodeURIComponent(agentMatch[1]),
158
+ agent,
104
159
  surface: agentMatch[2] ?? null,
105
160
  options,
106
161
  }).catch((error) => {
@@ -229,6 +284,32 @@ async function handleAgentRoute(request, response, context) {
229
284
  (0, mailbox_http_response_1.writeJson)(response, 200, view);
230
285
  return;
231
286
  }
287
+ if (surface === "habit-run-summaries") {
288
+ const limit = parseHabitRunLimit(request.url);
289
+ if (limit === null) {
290
+ (0, mailbox_http_response_1.writeJson)(response, 400, { ok: false, error: "limit must be an integer between 1 and 100" });
291
+ return;
292
+ }
293
+ const view = limit === undefined
294
+ ? options.hooks.readAgentHabitRunSummaries(agent)
295
+ : options.hooks.readAgentHabitRunSummaries(agent, { limit });
296
+ (0, mailbox_http_response_1.writeJson)(response, 200, view);
297
+ return;
298
+ }
299
+ if (surface === "habit-run-summary") {
300
+ const parsed = parseHabitSummarySelector(request.url);
301
+ if (!parsed.ok) {
302
+ (0, mailbox_http_response_1.writeJson)(response, 400, { ok: false, error: parsed.error });
303
+ return;
304
+ }
305
+ const summary = options.hooks.readAgentHabitRunSummary(agent, parsed.selector);
306
+ if (!summary) {
307
+ (0, mailbox_http_response_1.writeJson)(response, 404, { ok: false, error: "habit summary not found" });
308
+ return;
309
+ }
310
+ (0, mailbox_http_response_1.writeJson)(response, 200, summary);
311
+ return;
312
+ }
232
313
  if (surface.startsWith("habit-runs/")) {
233
314
  const rawRunId = surface.slice("habit-runs/".length);
234
315
  const runId = decodePathSegment(rawRunId);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readSelfFixView = exports.readMailboxContinuity = exports.readOrientationView = exports.readObligationDetailView = exports.readNoteDecisionView = exports.readSentinelView = exports.readContextLossGauntletView = exports.readChangesView = exports.readNeedsMeView = exports.readNotesView = exports.readLogView = exports.readHabitView = exports.readHabitRunView = exports.readHabitRunReceiptView = exports.readFriendView = exports.readDeskPrefs = exports.readDaemonHealthDeep = exports.readCodingDeep = exports.readBridgeInventory = exports.readAttentionView = exports.readMailView = exports.readMailMessageView = exports.readSessionTranscript = exports.readSessionInventory = exports.readMailboxMachineState = exports.readMailboxAgentState = exports.readObligationSummary = void 0;
3
+ exports.readSelfFixView = exports.readMailboxContinuity = exports.readOrientationView = exports.readObligationDetailView = exports.readNoteDecisionView = exports.readSentinelView = exports.readContextLossGauntletView = exports.readChangesView = exports.readNeedsMeView = exports.readNotesView = exports.readLogView = exports.readHabitView = exports.readHabitRunView = exports.readHabitRunReceiptView = exports.readHabitSessionSummaryView = exports.readHabitSessionSummaryListView = exports.readFriendView = exports.readDeskPrefs = exports.readDaemonHealthDeep = exports.readCodingDeep = exports.readBridgeInventory = exports.readAttentionView = exports.readMailView = exports.readMailMessageView = exports.readSessionTranscript = exports.readSessionInventory = exports.readMailboxMachineState = exports.readMailboxAgentState = exports.readObligationSummary = void 0;
4
4
  var agent_machine_1 = require("./readers/agent-machine");
5
5
  Object.defineProperty(exports, "readObligationSummary", { enumerable: true, get: function () { return agent_machine_1.readObligationSummary; } });
6
6
  Object.defineProperty(exports, "readMailboxAgentState", { enumerable: true, get: function () { return agent_machine_1.readMailboxAgentState; } });
@@ -18,6 +18,8 @@ Object.defineProperty(exports, "readCodingDeep", { enumerable: true, get: functi
18
18
  Object.defineProperty(exports, "readDaemonHealthDeep", { enumerable: true, get: function () { return runtime_readers_1.readDaemonHealthDeep; } });
19
19
  Object.defineProperty(exports, "readDeskPrefs", { enumerable: true, get: function () { return runtime_readers_1.readDeskPrefs; } });
20
20
  Object.defineProperty(exports, "readFriendView", { enumerable: true, get: function () { return runtime_readers_1.readFriendView; } });
21
+ Object.defineProperty(exports, "readHabitSessionSummaryListView", { enumerable: true, get: function () { return runtime_readers_1.readHabitSessionSummaryListView; } });
22
+ Object.defineProperty(exports, "readHabitSessionSummaryView", { enumerable: true, get: function () { return runtime_readers_1.readHabitSessionSummaryView; } });
21
23
  Object.defineProperty(exports, "readHabitRunReceiptView", { enumerable: true, get: function () { return runtime_readers_1.readHabitRunReceiptView; } });
22
24
  Object.defineProperty(exports, "readHabitRunView", { enumerable: true, get: function () { return runtime_readers_1.readHabitRunView; } });
23
25
  Object.defineProperty(exports, "readHabitView", { enumerable: true, get: function () { return runtime_readers_1.readHabitView; } });
@@ -127,14 +127,24 @@ function normalizeObligationCurrentSurface(currentSurface, updatedAt, liveCoding
127
127
  && (Date.now() - updatedAtMs) <= STALE_CODING_SURFACE_WINDOW_MS;
128
128
  return recentlyTouched ? currentSurface : null;
129
129
  }
130
+ function legacyObligationTimestamp(obligation) {
131
+ if (typeof obligation.updatedAt === "string")
132
+ return obligation.updatedAt;
133
+ if (typeof obligation.createdAt === "string")
134
+ return obligation.createdAt;
135
+ return "";
136
+ }
137
+ function legacyObligationStatus(obligation) {
138
+ return typeof obligation.status === "string" ? obligation.status : "pending";
139
+ }
130
140
  function readObligationSummary(agentRoot) {
131
141
  const liveCodingSurfaceLabels = buildLiveCodingSurfaceLabels(agentRoot);
132
142
  const items = (0, obligations_1.readPendingObligations)(agentRoot)
133
143
  .map((obligation) => {
134
- const updatedAt = obligation.updatedAt ?? obligation.createdAt;
144
+ const updatedAt = legacyObligationTimestamp(obligation);
135
145
  return {
136
146
  id: obligation.id,
137
- status: obligation.status,
147
+ status: legacyObligationStatus(obligation),
138
148
  content: obligation.content,
139
149
  updatedAt,
140
150
  nextAction: obligation.nextAction ?? null,
@@ -43,6 +43,8 @@ exports.readLogView = readLogView;
43
43
  exports.readHabitView = readHabitView;
44
44
  exports.readHabitRunView = readHabitRunView;
45
45
  exports.readHabitRunReceiptView = readHabitRunReceiptView;
46
+ exports.readHabitSessionSummaryListView = readHabitSessionSummaryListView;
47
+ exports.readHabitSessionSummaryView = readHabitSessionSummaryView;
46
48
  exports.readNeedsMeView = readNeedsMeView;
47
49
  exports.readDeskPrefs = readDeskPrefs;
48
50
  const fs = __importStar(require("fs"));
@@ -50,6 +52,7 @@ const path = __importStar(require("path"));
50
52
  const runtime_1 = require("../../../nerves/runtime");
51
53
  const habit_parser_1 = require("../../habits/habit-parser");
52
54
  const habit_runtime_state_1 = require("../../habits/habit-runtime-state");
55
+ const habit_session_summary_1 = require("../../habits/habit-session-summary");
53
56
  const identity_1 = require("../../identity");
54
57
  const daemon_health_1 = require("../../daemon/daemon-health");
55
58
  const flight_recorder_1 = require("../../../arc/flight-recorder");
@@ -656,6 +659,34 @@ function readHabitRunReceiptView(agentRoot, runId) {
656
659
  });
657
660
  return receipt ? { receipt } : null;
658
661
  }
662
+ function readHabitSessionSummaryListView(agentRoot, options = {}) {
663
+ const limit = normalizeHabitRunLimit(options.limit);
664
+ const receipts = (0, flight_recorder_1.listHabitRunReceipts)(agentRoot);
665
+ const items = receipts.slice(0, limit)
666
+ .map((receipt) => (0, habit_session_summary_1.readHabitSessionSummary)(agentRoot, { runId: receipt.runId }))
667
+ .filter((summary) => summary !== null);
668
+ (0, runtime_1.emitNervesEvent)({
669
+ component: "heart",
670
+ event: "heart.mailbox_habit_run_summaries_read",
671
+ message: "reading mailbox habit run summaries",
672
+ meta: { agentRoot, totalCount: receipts.length, limit, itemCount: items.length },
673
+ });
674
+ return {
675
+ totalCount: receipts.length,
676
+ limit,
677
+ items,
678
+ };
679
+ }
680
+ function readHabitSessionSummaryView(agentRoot, selector) {
681
+ const summary = (0, habit_session_summary_1.readHabitSessionSummary)(agentRoot, selector);
682
+ (0, runtime_1.emitNervesEvent)({
683
+ component: "heart",
684
+ event: "heart.mailbox_habit_run_summary_read",
685
+ message: "reading mailbox habit run summary",
686
+ meta: { agentRoot, runId: summary?.runId ?? null, habitName: summary?.habitName ?? selector.habitName ?? null, found: summary !== null },
687
+ });
688
+ return summary;
689
+ }
659
690
  function readNeedsMeView(agentName, options = {}) {
660
691
  const bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
661
692
  const now = options.now?.() ?? new Date();
@@ -134,14 +134,18 @@ function scanArcSourceIssues(agentRoot) {
134
134
  }
135
135
  function obligationItem(obligation) {
136
136
  const freshness = obligation.meaning?.stalenessClass === "at-risk" ? "stale_risky" : "current";
137
+ const status = typeof obligation.status === "string" ? obligation.status : "pending";
138
+ const updatedAt = typeof obligation.updatedAt === "string"
139
+ ? obligation.updatedAt
140
+ : typeof obligation.createdAt === "string" ? obligation.createdAt : undefined;
137
141
  return {
138
142
  id: obligation.id,
139
143
  title: obligation.content,
140
- status: obligation.status,
144
+ status,
141
145
  source: source("obligation", obligationLocator(obligation.id), freshness),
142
146
  ...(obligation.latestNote ? { summary: obligation.latestNote } : {}),
143
147
  ...(obligation.nextAction ? { nextAction: obligation.nextAction } : {}),
144
- updatedAt: obligation.updatedAt ?? obligation.createdAt,
148
+ ...(updatedAt ? { updatedAt } : {}),
145
149
  };
146
150
  }
147
151
  function returnObligationItem(obligation) {
@@ -51,11 +51,13 @@ const coding_1 = require("./coding");
51
51
  const pending_1 = require("../mind/pending");
52
52
  const obligations_1 = require("../arc/obligations");
53
53
  const progress_story_1 = require("../heart/progress-story");
54
+ const habit_session_summary_1 = require("../heart/habits/habit-session-summary");
54
55
  const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
55
56
  const mail_import_discovery_1 = require("../heart/mail-import-discovery");
56
57
  const outbound_1 = require("../senses/voice/outbound");
57
58
  const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
58
59
  const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
60
+ const VALID_HABIT_SUMMARY_WHICH = new Set(["latest", "previous", "latest-success", "latest-failure"]);
59
61
  async function summarizeSessionTailSafely(options) {
60
62
  try {
61
63
  return await (0, session_transcript_1.summarizeSessionTail)(options);
@@ -99,6 +101,79 @@ function normalizeProgressOutcome(text) {
99
101
  }
100
102
  return trimmed;
101
103
  }
104
+ function optionalArg(args, key) {
105
+ const value = args[key];
106
+ if (value === undefined || value === null)
107
+ return undefined;
108
+ if (typeof value !== "string")
109
+ return null;
110
+ const trimmed = value.trim();
111
+ return trimmed ? trimmed : undefined;
112
+ }
113
+ function validateSessionSummarySelector(args) {
114
+ const runId = optionalArg(args, "runId");
115
+ const habitName = optionalArg(args, "habitName");
116
+ const operationId = optionalArg(args, "operationId");
117
+ const which = optionalArg(args, "which");
118
+ if (runId === null || habitName === null || operationId === null || which === null) {
119
+ return {
120
+ ok: false,
121
+ code: which === null ? "invalid_which" : "selector_required",
122
+ message: which === null
123
+ ? "which must be latest, previous, latest-success, or latest-failure"
124
+ : "selector fields must be strings",
125
+ };
126
+ }
127
+ if (runId !== undefined) {
128
+ if (habitName !== undefined || operationId !== undefined || which !== undefined) {
129
+ return {
130
+ ok: false,
131
+ code: "run_id_exclusive",
132
+ message: "runId cannot be combined with habitName, operationId, or which",
133
+ };
134
+ }
135
+ return { ok: true, selector: { runId } };
136
+ }
137
+ if (habitName === undefined && operationId === undefined) {
138
+ return {
139
+ ok: false,
140
+ code: "selector_required",
141
+ message: "provide runId, habitName, or operationId",
142
+ };
143
+ }
144
+ if (which !== undefined && !VALID_HABIT_SUMMARY_WHICH.has(which)) {
145
+ return {
146
+ ok: false,
147
+ code: "invalid_which",
148
+ message: "which must be latest, previous, latest-success, or latest-failure",
149
+ };
150
+ }
151
+ return {
152
+ ok: true,
153
+ selector: {
154
+ ...(habitName !== undefined ? { habitName } : {}),
155
+ ...(operationId !== undefined ? { operationId } : {}),
156
+ ...(which !== undefined ? { which } : {}),
157
+ },
158
+ };
159
+ }
160
+ function renderSessionSummaryText(summary) {
161
+ const lines = [
162
+ `habit ${summary.habitName} run ${summary.runId} finished with ${summary.status}.`,
163
+ summary.operationId ? `operation: ${summary.operationId}` : null,
164
+ summary.summary,
165
+ summary.nextLikelyStep ? `next: ${summary.nextLikelyStep}` : null,
166
+ summary.decisions.length > 0 ? `decisions: ${summary.decisions.join("; ")}` : null,
167
+ summary.pending.count > 0 ? `pending: ${summary.pending.count} file(s) (${summary.pending.files.join(", ")})` : "pending: none",
168
+ summary.messagesSent.length > 0 ? `messages: ${summary.messagesSent.length}` : "messages: none",
169
+ summary.toolsUsed.length > 0 ? `tools: ${summary.toolsUsed.join(", ")}` : "tools: none",
170
+ summary.errors.length > 0 ? `errors: ${summary.errors.join("; ")}` : null,
171
+ summary.warnings.length > 0 ? `warnings: ${summary.warnings.join("; ")}` : null,
172
+ `receipt: ${summary.sources.receipt}`,
173
+ `session: ${summary.sources.session}`,
174
+ ];
175
+ return lines.filter((line) => Boolean(line)).join("\n");
176
+ }
102
177
  function writePendingEnvelope(queueDir, message) {
103
178
  fs.mkdirSync(queueDir, { recursive: true });
104
179
  const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
@@ -369,6 +444,57 @@ exports.sessionToolDefinitions = [
369
444
  return `this is my current top-level live world-state.\nanswer whole-self status questions from this before drilling into individual sessions.\n\n${(0, active_work_1.formatActiveWorkFrame)(frame)}`;
370
445
  },
371
446
  },
447
+ {
448
+ tool: {
449
+ type: "function",
450
+ function: {
451
+ name: "session_summary",
452
+ description: "read-only orientation for habit runs. returns a structured live summary from habit receipts, session files, pending dirs, and runtime cursors without writing state.",
453
+ parameters: {
454
+ type: "object",
455
+ properties: {
456
+ runId: { type: "string", description: "exact habit run id; cannot be combined with habitName, operationId, or which" },
457
+ habitName: { type: "string", description: "habit name to select from" },
458
+ operationId: { type: "string", description: "operation id for stateful habit run groups, such as habit:heartbeat" },
459
+ which: {
460
+ type: "string",
461
+ enum: ["latest", "previous", "latest-success", "latest-failure"],
462
+ description: "which matching run to read; defaults to latest",
463
+ },
464
+ },
465
+ },
466
+ },
467
+ },
468
+ handler: (args) => {
469
+ const validation = validateSessionSummarySelector(args);
470
+ if (!validation.ok) {
471
+ return JSON.stringify({
472
+ kind: "invalid_selector",
473
+ code: validation.code,
474
+ message: validation.message,
475
+ }, null, 2);
476
+ }
477
+ const summary = (0, habit_session_summary_1.readHabitSessionSummary)((0, identity_1.getAgentRoot)(), validation.selector);
478
+ if (!summary) {
479
+ return JSON.stringify({
480
+ kind: "not_found",
481
+ message: "no habit run matched selector",
482
+ selector: validation.selector,
483
+ }, null, 2);
484
+ }
485
+ return JSON.stringify({
486
+ kind: "habit_session_summary",
487
+ text: renderSessionSummaryText(summary),
488
+ summary,
489
+ }, null, 2);
490
+ },
491
+ riskProfile: {
492
+ mutates: "none",
493
+ risk: "low",
494
+ reason: "reads habit run summaries from local receipts and session artifacts",
495
+ },
496
+ summaryKeys: ["runId", "habitName", "operationId", "which"],
497
+ },
372
498
  {
373
499
  tool: {
374
500
  type: "function",
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildHabitTurnMessage = buildHabitTurnMessage;
4
4
  const runtime_1 = require("../nerves/runtime");
5
+ const PRIOR_SESSION_SUMMARY_LIMIT = 1600;
6
+ const PRIOR_SESSION_TRUNCATION_SUFFIX = "\n[truncated]";
5
7
  function formatElapsed(ms) {
6
8
  const minutes = Math.floor(ms / 60000);
7
9
  if (minutes < 60) {
@@ -11,9 +13,9 @@ function formatElapsed(ms) {
11
13
  return `${hours} ${hours === 1 ? "hour" : "hours"}`;
12
14
  }
13
15
  function buildHabitTurnMessage(options) {
14
- const { habitName, habitTitle, habitBody, lastRun, checkpoint, alsoDue, staleObligations, parseErrors, degradedComponents, arcResume, deskOrientation, surfacePolicy, now, } = options;
16
+ const { habitName, habitTitle, habitBody, lastRun, checkpoint, alsoDue, staleObligations, parseErrors, degradedComponents, arcResume, deskOrientation, surfacePolicy, priorSessionSummary, now, } = options;
15
17
  const hasBody = habitBody !== undefined && habitBody !== "";
16
- const leadingSections = buildLeadingSections(arcResume, deskOrientation, surfacePolicy);
18
+ const leadingSections = buildLeadingSections(arcResume, deskOrientation, surfacePolicy, priorSessionSummary);
17
19
  // First beat: lastRun is null
18
20
  if (lastRun === null) {
19
21
  // Cold start: no checkpoint, no body — bare awareness
@@ -86,7 +88,7 @@ function buildHabitTurnMessage(options) {
86
88
  });
87
89
  return joinSections(leadingSections, sections);
88
90
  }
89
- function buildLeadingSections(arcResume, deskOrientation, surfacePolicy) {
91
+ function buildLeadingSections(arcResume, deskOrientation, surfacePolicy, priorSessionSummary) {
90
92
  const sections = [];
91
93
  if (arcResume?.trim())
92
94
  sections.push(arcResume.trim());
@@ -94,11 +96,47 @@ function buildLeadingSections(arcResume, deskOrientation, surfacePolicy) {
94
96
  sections.push(deskOrientation.trim());
95
97
  if (surfacePolicy?.trim())
96
98
  sections.push(surfacePolicy.trim());
99
+ const priorSummary = buildPriorSessionSummarySection(priorSessionSummary);
100
+ if (priorSummary)
101
+ sections.push(priorSummary);
97
102
  return sections;
98
103
  }
99
104
  function joinSections(leadingSections, sections) {
100
105
  return [...leadingSections, ...sections].filter((section) => section.trim().length > 0).join("\n\n");
101
106
  }
107
+ function truncatePriorSummary(summary) {
108
+ if (summary.length <= PRIOR_SESSION_SUMMARY_LIMIT)
109
+ return summary;
110
+ return `${summary.slice(0, PRIOR_SESSION_SUMMARY_LIMIT - PRIOR_SESSION_TRUNCATION_SUFFIX.length)}${PRIOR_SESSION_TRUNCATION_SUFFIX}`;
111
+ }
112
+ function buildPriorSessionSummarySection(priorSessionSummary) {
113
+ if (!priorSessionSummary || priorSessionSummary.mode !== "stateful")
114
+ return null;
115
+ const lines = ["## Prior habit session summary"];
116
+ const summary = priorSessionSummary.summary?.trim();
117
+ if (summary) {
118
+ lines.push(truncatePriorSummary(summary));
119
+ }
120
+ else {
121
+ lines.push("No prior stateful habit summary found for this operation.");
122
+ }
123
+ const sourceLines = ["receipt", "session", "pending", "runtimeState"]
124
+ .flatMap((key) => {
125
+ const value = cleanSourceLocator(priorSessionSummary.sources[key]);
126
+ return value ? [`${key}: ${value}`] : [];
127
+ });
128
+ if (sourceLines.length > 0)
129
+ lines.push(...sourceLines);
130
+ if (priorSessionSummary.warnings.length > 0)
131
+ lines.push(`warnings: ${priorSessionSummary.warnings.join("; ")}`);
132
+ return lines.join("\n");
133
+ }
134
+ function cleanSourceLocator(value) {
135
+ if (typeof value !== "string")
136
+ return null;
137
+ const trimmed = value.trim();
138
+ return trimmed.length > 0 ? trimmed : null;
139
+ }
102
140
  function appendTrailingExtras(sections, alsoDue, staleObligations, parseErrors, degradedComponents) {
103
141
  // 4. Also-due
104
142
  if (alsoDue) {
@@ -43,8 +43,10 @@ const runtime_1 = require("../nerves/runtime");
43
43
  const identity_1 = require("../heart/identity");
44
44
  const pending_1 = require("../mind/pending");
45
45
  const habit_parser_1 = require("../heart/habits/habit-parser");
46
+ const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
46
47
  const habit_session_1 = require("../heart/habits/habit-session");
47
48
  const flight_recorder_1 = require("../arc/flight-recorder");
49
+ const habit_session_summary_1 = require("../heart/habits/habit-session-summary");
48
50
  const store_file_1 = require("../mind/friends/store-file");
49
51
  const tools_base_1 = require("../repertoire/tools-base");
50
52
  const tools_surface_1 = require("../repertoire/tools-surface");
@@ -89,6 +91,79 @@ function isHeartbeatOkRestResult(result) {
89
91
  const maybeResult = result;
90
92
  return maybeResult.turnOutcome === "rested" && maybeResult.restStatus === "HEARTBEAT_OK";
91
93
  }
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
+ }
92
167
  function fallbackHabitFile(habitName) {
93
168
  return {
94
169
  name: habitName,
@@ -100,6 +175,7 @@ function fallbackHabitFile(habitName) {
100
175
  tools: [],
101
176
  origin: null,
102
177
  surface: { family: false, originator: false, extra: [] },
178
+ continuity: { mode: "fresh" },
103
179
  body: "",
104
180
  };
105
181
  }
@@ -124,8 +200,10 @@ function readHabitForRun(agentRoot, habitName, errors) {
124
200
  async function prepareHabitRun(habitName, trigger, startedAt) {
125
201
  const agentRoot = (0, identity_1.getAgentRoot)();
126
202
  const errors = [];
127
- const habit = readHabitForRun(agentRoot, habitName, errors);
203
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, readHabitForRun(agentRoot, habitName, errors));
128
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);
129
207
  const paths = (0, habit_session_1.createHabitSessionPaths)(agentRoot, runId, habit.name);
130
208
  const friendStore = new store_file_1.FileFriendStore(path.join(agentRoot, "friends"));
131
209
  const permissionEnvelope = await (0, habit_session_1.normalizeHabitPermissionEnvelope)(habit, { agentRoot, friendStore });
@@ -134,8 +212,10 @@ async function prepareHabitRun(habitName, trigger, startedAt) {
134
212
  agentRoot,
135
213
  habit,
136
214
  runId,
215
+ operationId,
137
216
  trigger,
138
217
  startedAt,
218
+ priorSessionSummary,
139
219
  paths,
140
220
  permissionEnvelope,
141
221
  toolPolicy,
@@ -150,6 +230,29 @@ function riskProfileForHabitPolicy(definition, name) {
150
230
  const probeArgs = name === "shell" ? { command: "touch /tmp/habit-policy-probe" } : {};
151
231
  return (0, tools_1.riskProfileForTool)(definition, name, probeArgs);
152
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
+ }
153
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()) {
154
257
  let running = false;
155
258
  const queue = [];
@@ -164,11 +267,13 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
164
267
  trigger: habitRun.trigger,
165
268
  startedAt: habitRun.startedAt,
166
269
  endedAt,
270
+ operationId: habitRun.operationId,
167
271
  permissionEnvelope: habitRun.permissionEnvelope,
168
272
  toolPolicy: habitRun.toolPolicy,
169
273
  producedRefs: habitRun.producedRefs,
170
274
  surfaceAttempts: habitRun.surfaceAttempts,
171
275
  errors: habitRun.errors,
276
+ summarySnapshot: deriveHabitSummarySnapshot(habitRun),
172
277
  });
173
278
  }
174
279
  function clearHeartbeatRestShield() {
@@ -286,6 +391,13 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
286
391
  ...(currentHabitRun
287
392
  ? {
288
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
+ },
289
401
  habitSession: {
290
402
  runId: currentHabitRun.runId,
291
403
  sessionPath: currentHabitRun.paths.sessionPath,