@jungjaehoon/mama-os 0.57.0 → 0.58.0

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/dist/api/attachment-actions.d.ts.map +1 -1
  4. package/dist/api/attachment-actions.js +5 -3
  5. package/dist/api/attachment-actions.js.map +1 -1
  6. package/dist/api/help-actions.d.ts +12 -0
  7. package/dist/api/help-actions.d.ts.map +1 -0
  8. package/dist/api/help-actions.js +70 -0
  9. package/dist/api/help-actions.js.map +1 -0
  10. package/dist/api/owner-timezone-actions.d.ts.map +1 -1
  11. package/dist/api/owner-timezone-actions.js +1 -1
  12. package/dist/api/owner-timezone-actions.js.map +1 -1
  13. package/dist/api/record-actions.d.ts +8 -0
  14. package/dist/api/record-actions.d.ts.map +1 -0
  15. package/dist/api/record-actions.js +53 -0
  16. package/dist/api/record-actions.js.map +1 -0
  17. package/dist/api/report-actions.d.ts +0 -2
  18. package/dist/api/report-actions.d.ts.map +1 -1
  19. package/dist/api/report-actions.js +5 -5
  20. package/dist/api/report-actions.js.map +1 -1
  21. package/dist/api/report-source-actions.js +2 -2
  22. package/dist/api/report-source-actions.js.map +1 -1
  23. package/dist/api/viewer-server.d.ts +1 -0
  24. package/dist/api/viewer-server.d.ts.map +1 -1
  25. package/dist/api/viewer-server.js +21 -17
  26. package/dist/api/viewer-server.js.map +1 -1
  27. package/dist/api/work-actions.d.ts +0 -13
  28. package/dist/api/work-actions.d.ts.map +1 -1
  29. package/dist/api/work-actions.js +6 -37
  30. package/dist/api/work-actions.js.map +1 -1
  31. package/dist/cli/commands/daemon.d.ts.map +1 -1
  32. package/dist/cli/commands/daemon.js +14 -2
  33. package/dist/cli/commands/daemon.js.map +1 -1
  34. package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
  35. package/dist/connectors/framework/polling-scheduler.js +9 -0
  36. package/dist/connectors/framework/polling-scheduler.js.map +1 -1
  37. package/dist/gateways/telegram-format.js +2 -2
  38. package/dist/gateways/telegram-format.js.map +1 -1
  39. package/dist/operator/board-read-views.d.ts.map +1 -1
  40. package/dist/operator/board-read-views.js +3 -3
  41. package/dist/operator/board-read-views.js.map +1 -1
  42. package/dist/operator/board-slot-instructions.d.ts +4 -6
  43. package/dist/operator/board-slot-instructions.d.ts.map +1 -1
  44. package/dist/operator/board-slot-instructions.js +11 -24
  45. package/dist/operator/board-slot-instructions.js.map +1 -1
  46. package/dist/runtime/action-mcp-server.d.ts.map +1 -1
  47. package/dist/runtime/action-mcp-server.js +4 -5
  48. package/dist/runtime/action-mcp-server.js.map +1 -1
  49. package/dist/runtime/action-surface.d.ts +1 -1
  50. package/dist/runtime/action-surface.d.ts.map +1 -1
  51. package/dist/runtime/action-surface.js +13 -3
  52. package/dist/runtime/action-surface.js.map +1 -1
  53. package/dist/runtime/native-session.d.ts +6 -0
  54. package/dist/runtime/native-session.d.ts.map +1 -1
  55. package/dist/runtime/native-session.js +13 -15
  56. package/dist/runtime/native-session.js.map +1 -1
  57. package/dist/runtime/owner-runtime.d.ts +14 -3
  58. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  59. package/dist/runtime/owner-runtime.js +77 -13
  60. package/dist/runtime/owner-runtime.js.map +1 -1
  61. package/dist/runtime/owner-system-prompt.d.ts +7 -8
  62. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  63. package/dist/runtime/owner-system-prompt.js +64 -68
  64. package/dist/runtime/owner-system-prompt.js.map +1 -1
  65. package/dist/runtime/recent-owner-exchanges.d.ts +0 -2
  66. package/dist/runtime/recent-owner-exchanges.d.ts.map +1 -1
  67. package/dist/runtime/recent-owner-exchanges.js +0 -26
  68. package/dist/runtime/recent-owner-exchanges.js.map +1 -1
  69. package/dist/runtime/record-orders.d.ts +49 -0
  70. package/dist/runtime/record-orders.d.ts.map +1 -0
  71. package/dist/runtime/record-orders.js +152 -0
  72. package/dist/runtime/record-orders.js.map +1 -0
  73. package/dist/runtime/report-scheduler.d.ts.map +1 -1
  74. package/dist/runtime/report-scheduler.js +14 -11
  75. package/dist/runtime/report-scheduler.js.map +1 -1
  76. package/dist/runtime/stimulus-delivery.d.ts +25 -27
  77. package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
  78. package/dist/runtime/stimulus-delivery.js +185 -268
  79. package/dist/runtime/stimulus-delivery.js.map +1 -1
  80. package/dist/runtime/timezone.d.ts +2 -0
  81. package/dist/runtime/timezone.d.ts.map +1 -1
  82. package/dist/runtime/timezone.js +9 -0
  83. package/dist/runtime/timezone.js.map +1 -1
  84. package/dist/runtime/turn-orders.d.ts +86 -0
  85. package/dist/runtime/turn-orders.d.ts.map +1 -0
  86. package/dist/runtime/turn-orders.js +272 -0
  87. package/dist/runtime/turn-orders.js.map +1 -0
  88. package/package.json +2 -2
  89. package/dist/runtime/report-prompts.d.ts +0 -14
  90. package/dist/runtime/report-prompts.d.ts.map +0 -1
  91. package/dist/runtime/report-prompts.js +0 -69
  92. package/dist/runtime/report-prompts.js.map +0 -1
@@ -5,14 +5,14 @@ exports.sourceDeltaStimulusId = sourceDeltaStimulusId;
5
5
  exports.createStimulusIntake = createStimulusIntake;
6
6
  exports.renderWindowQueue = renderWindowQueue;
7
7
  exports.stimulusFailureReason = stimulusFailureReason;
8
+ exports.isRecordOrderRow = isRecordOrderRow;
8
9
  exports.createStimulusDelivery = createStimulusDelivery;
9
10
  const untrusted_content_js_1 = require("../utils/untrusted-content.js");
10
11
  const node_crypto_1 = require("node:crypto");
11
12
  const canonicalize_1 = require("@jungjaehoon/mama-core/canonicalize");
12
- const recent_owner_exchanges_js_1 = require("./recent-owner-exchanges.js");
13
13
  const owner_system_prompt_js_1 = require("./owner-system-prompt.js");
14
- const report_prompts_js_1 = require("./report-prompts.js");
15
- const work_actions_js_1 = require("../api/work-actions.js");
14
+ const turn_orders_js_1 = require("./turn-orders.js");
15
+ const timezone_js_1 = require("./timezone.js");
16
16
  exports.OWNER_RUNTIME_SESSION_KEY = 'owner:runtime';
17
17
  function sourceObservationHandle(ref) {
18
18
  if (typeof ref.observationRef !== 'string' || ref.observationRef.trim() === '') {
@@ -122,14 +122,6 @@ function createStimulusIntake(runtime, principalId) {
122
122
  occurredAt: input.occurredAt,
123
123
  ...(input.payload === undefined ? {} : { payload: input.payload }),
124
124
  }),
125
- acceptNativeEvent: (input) => runtime.accept({
126
- id: input.id,
127
- kind: 'native_event',
128
- principalId,
129
- channelKey: input.channelKey,
130
- occurredAt: input.occurredAt,
131
- ...(input.payload === undefined ? {} : { payload: input.payload }),
132
- }),
133
125
  };
134
126
  }
135
127
  function payloadCarriesMessageText(payload) {
@@ -139,22 +131,6 @@ function payloadCarriesMessageText(payload) {
139
131
  return (Array.isArray(refs) &&
140
132
  refs.some((ref) => ref !== null && typeof ref === 'object' && !Array.isArray(ref) && 'contentPreview' in ref));
141
133
  }
142
- function localStamp(iso, timeZone) {
143
- const ms = Date.parse(iso);
144
- if (!Number.isFinite(ms))
145
- throw new Error(`A message line needs a source time, got ${iso}`);
146
- const values = Object.fromEntries(new Intl.DateTimeFormat('en-CA', {
147
- timeZone,
148
- month: '2-digit',
149
- day: '2-digit',
150
- hour: '2-digit',
151
- minute: '2-digit',
152
- hourCycle: 'h23',
153
- })
154
- .formatToParts(new Date(ms))
155
- .map((part) => [part.type, part.value]));
156
- return `${values.month}-${values.day} ${values.hour}:${values.minute}`;
157
- }
158
134
  function textField(value) {
159
135
  return typeof value === 'string' ? value : '';
160
136
  }
@@ -179,7 +155,7 @@ function messageLines(payload, timeZone) {
179
155
  const channel = channelName
180
156
  ? `${textField(ref.connector)}:${channelName}`
181
157
  : textField(ref.channel) || textField(ref.connector);
182
- lines.push(`[${localStamp(textField(ref.sourceAt), timeZone.get())}] ${channel} · ${author} · ${textField(ref.observationRef)}: ${text}`);
158
+ lines.push(`[${(0, timezone_js_1.localStamp)(textField(ref.sourceAt), timeZone.get())}] ${channel} · ${author} · ${textField(ref.observationRef)}: ${text}`);
183
159
  }
184
160
  return lines;
185
161
  }
@@ -237,216 +213,67 @@ function renderWindowQueue(queue) {
237
213
  }
238
214
  return (0, untrusted_content_js_1.wrapUntrustedContent)('source_delta', lines.join('\n'));
239
215
  }
240
- function boundedStimulus(row, liveSourceDelta, options, candidates = []) {
241
- if (row.kind === 'scheduled')
242
- return (0, report_prompts_js_1.buildScheduledReportPrompt)(row.payload, new Date(row.occurredAt), {
243
- wikiEnabled: options.wikiEnabled,
244
- messenger: options.formattingRoutes?.reports,
245
- timeZone: options.timeZone.get(),
246
- });
216
+ /**
217
+ * Instructions a replay window needs and live turns do not: the window's orchestration and its
218
+ * recording rules, moved here from the standing prompt.
219
+ */
220
+ function replayInstructions(backend, wikiEnabled) {
221
+ const action = (name) => (0, owner_system_prompt_js_1.actionName)(backend, name);
222
+ return [
223
+ 'replay_instructions:',
224
+ `- You are the orchestrator of this window and must know what happened. Note the time your turn starts. Plan from sections A, B, C, suspected duplicates and unresolved; decide new work (C) yourself and give it an owner; give each native subagent a disjoint set of work items (with their full source lines, history and current revisions) and the topic pages it owns, and wait for every receipt (each commitmentId with revision before and after, created commitmentIds, topic pages updated, anything it could not do). Then read back with ${action('work.list')} view=items changedSince=<your turn start>, compare it with the receipts, and settle gaps, conflicts and duplicates yourself. Only then write the journal's judgment section, the board, Home.md and lessons. The window's current_work already lists every item with its current revision; do not list the whole ledger again. Each subagent adds one entry per moved item to daily/YYYY-MM-DD.md, grouped by project, which you create before dispatching.`,
225
+ `- ${owner_system_prompt_js_1.SUBAGENT_RUNTIME_RULES[backend].replace('only when an order asks for one', 'for this window')} When a subagent finishes you verify and integrate its result and do not spawn another for the same objective.`,
226
+ `- For a moved item, revise or create the work item (${action('work.revise')}, ${action('work.create')}) with a summary of what changed and why, derived_from links to its observations, and the assignee and roles the evidence points to. Record "unconfirmed" only when no observation points to anyone. Set eventDatetime to the source event time, not replay time. When current_work supplies a revision, pass it as expectedRevision; for another write in the same window use the revision the previous write returned.`,
227
+ wikiEnabled
228
+ ? `- With end_of_window_instructions, finish the day's work changes before updating each affected board section with ${action('report.publish')} and topic wiki page with ${action('manage.wiki.update')}.`
229
+ : `- With end_of_window_instructions, finish the day's work changes before updating each affected board section with ${action('report.publish')}.`,
230
+ '- A replay window is history and is not delivered to the owner: it ends without a marker.',
231
+ ];
232
+ }
233
+ /** The replay window text: the window material and its instructions. */
234
+ function replayWindowText(row, options) {
247
235
  const lines = [
248
- '## Bounded stimulus',
236
+ '## Replay window',
249
237
  `owner timezone: ${options.timeZone.get()}`,
250
- `kind: ${row.kind ?? 'unknown'}`,
251
238
  `stimulus_id: ${row.stimulusId}`,
252
239
  `channel: ${row.channelKey}`,
253
240
  `occurred_at: ${new Date(row.occurredAt).toISOString()}`,
254
- `preview: ${row.kind === 'source_delta' ? (0, untrusted_content_js_1.wrapUntrustedContent)('source_delta', JSON.stringify(row.preview)) : JSON.stringify(row.preview)}`,
241
+ 'delivery: replay window, not delivered to the owner',
242
+ ...replayInstructions(options.backend, options.wikiEnabled ?? false),
255
243
  ];
256
- if (row.kind === 'owner_message') {
257
- const messenger = row.stimulusId.split(':', 1)[0];
258
- lines.push(`messenger: ${messenger}`);
259
- const payload = row.payload;
260
- const input = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload.input : undefined;
261
- const attachments = input && typeof input === 'object' && !Array.isArray(input) ? input.attachments : undefined;
262
- if (Array.isArray(attachments)) {
263
- for (const attachment of attachments) {
264
- if (!attachment || typeof attachment !== 'object' || Array.isArray(attachment))
265
- continue;
266
- const name = JSON.stringify(attachment.name);
267
- if (typeof attachment.error === 'string') {
268
- lines.push(`attachment: name=${name} error=${JSON.stringify(attachment.error)}`);
269
- }
270
- else {
271
- lines.push(`attachment: name=${name} path=${JSON.stringify(attachment.path)}${typeof attachment.size === 'number' ? ` size=${attachment.size} bytes` : ''}`);
272
- }
273
- }
274
- }
275
- }
276
- const messages = row.kind === 'source_delta' ? messageLines(row.payload, options.timeZone) : null;
244
+ const messages = messageLines(row.payload, options.timeZone);
277
245
  if (messages === null)
278
246
  lines.push(`refs: ${JSON.stringify(row.refs)}`);
279
- if (row.kind === 'source_delta') {
280
- if (liveSourceDelta) {
281
- lines.push(`response_routing: ${(0, owner_system_prompt_js_1.liveDeltaRoutingInstruction)(options.backend ?? 'codex', options.wikiEnabled)}`);
282
- lines.push(`formatting: ${options.formattingRoutes?.notifications ?? 'telegram'}`);
283
- if (candidates.length > 0)
284
- lines.push('candidates (you decide):', ...candidates);
247
+ lines.push(row.refs.length === 0
248
+ ? 'source_read: this replay window has no source messages.'
249
+ : messages !== null
250
+ ? 'source_read: each message line below carries its full text; use the source read action with observationRefs only for a raw record or attachment you need, batched per connector (the first segment of the channel) with source set to that connector.'
251
+ : 'source_read: read these refs with the source read action, in one batched call with observationRefs; content remains bounded per ref.');
252
+ const payload = row.payload;
253
+ const replay = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload.replay : undefined;
254
+ if (replay && typeof replay === 'object' && !Array.isArray(replay)) {
255
+ const instructions = replay.endInstructions;
256
+ if (typeof instructions === 'string' && instructions.trim() !== '') {
257
+ lines.push(`window_end_instructions: ${instructions}`);
285
258
  }
286
- lines.push(row.refs.length === 0
287
- ? 'source_read: this replay window has no source messages.'
288
- : messages !== null
289
- ? 'source_read: each message line below carries its full text (a Trello action rendered from its record); use the source read action named in your standing instructions with observationRefs only for a raw record or attachment you need, batched per connector (the first segment of the channel) with source set to that connector.'
290
- : 'source_read: read these delta refs with the source read action named in your standing instructions, in one batched call with observationRefs; content remains bounded per ref.');
291
- const payload = row.payload;
292
- const replay = payload && typeof payload === 'object' && !Array.isArray(payload)
293
- ? payload.replay
294
- : undefined;
295
- if (replay && typeof replay === 'object' && !Array.isArray(replay)) {
296
- const instructions = replay.endInstructions;
297
- if (typeof instructions === 'string' && instructions.trim() !== '') {
298
- lines.push(`window_end_instructions: ${instructions}`);
299
- }
300
- if (replay.queue !== undefined) {
301
- lines.push('window_queue:', renderWindowQueue(replay.queue));
302
- if (messages !== null) {
303
- const work = ledgerLines(replay);
304
- lines.push(`current_work (commitmentId | revision | title | stage | status | assignee | lastEventTime), ${String(work.length)} items:`, ...work);
305
- }
306
- return lines.join('\n');
259
+ if (replay.queue !== undefined) {
260
+ lines.push('window_queue:', renderWindowQueue(replay.queue));
261
+ if (messages !== null) {
262
+ const work = ledgerLines(replay);
263
+ lines.push(`current_work (commitmentId | revision | title | stage | status | assignee | lastEventTime), ${String(work.length)} items:`, ...work);
307
264
  }
308
- }
309
- if (messages !== null) {
310
- const work = ledgerLines(replay);
311
- lines.push(`current_work (commitmentId | revision | title | stage | status | assignee | lastEventTime), ${String(work.length)} items:`, ...work, `messages (${options.timeZone.get()}, channel · sender · observationRef: text), ${String(messages.length)} lines:`, ...messages.map((message) => (0, untrusted_content_js_1.wrapUntrustedContent)('source_delta', message)));
312
265
  return lines.join('\n');
313
266
  }
314
267
  }
268
+ if (messages !== null) {
269
+ const work = ledgerLines(replay);
270
+ lines.push(`current_work (commitmentId | revision | title | stage | status | assignee | lastEventTime), ${String(work.length)} items:`, ...work, `messages (${options.timeZone.get()}, channel · sender · observationRef: text), ${String(messages.length)} lines:`, ...messages.map((message) => (0, untrusted_content_js_1.wrapUntrustedContent)('source_delta', message)));
271
+ return lines.join('\n');
272
+ }
315
273
  if (row.payload !== undefined)
316
- lines.push(`payload: ${row.kind === 'source_delta' ? (0, untrusted_content_js_1.wrapUntrustedContent)('source_delta', JSON.stringify(row.payload)) : JSON.stringify(row.payload)}`);
274
+ lines.push(`payload: ${(0, untrusted_content_js_1.wrapUntrustedContent)('source_delta', JSON.stringify(row.payload))}`);
317
275
  return lines.join('\n');
318
276
  }
319
- function sourcePayloadObject(row) {
320
- return row.payload && typeof row.payload === 'object' && !Array.isArray(row.payload)
321
- ? row.payload
322
- : null;
323
- }
324
- function payloadRefs(payload) {
325
- return Array.isArray(payload.refs)
326
- ? payload.refs.filter((ref) => ref !== null && typeof ref === 'object' && !Array.isArray(ref))
327
- : [];
328
- }
329
- /** The delta's text: its bounded preview lines (what the collector saw) and any ref previews. */
330
- function stimulusText(row) {
331
- const payload = sourcePayloadObject(row);
332
- if (!payload)
333
- return '';
334
- const preview = Array.isArray(payload.preview)
335
- ? payload.preview.filter((line) => typeof line === 'string')
336
- : [];
337
- return [...preview, ...payloadRefs(payload).map((ref) => textField(ref.contentPreview))]
338
- .filter(Boolean)
339
- .join(' ');
340
- }
341
- /** The delta's channel key and the channel labels its refs carry. */
342
- function stimulusChannels(row) {
343
- const payload = sourcePayloadObject(row);
344
- if (!payload)
345
- return new Set();
346
- return new Set([
347
- textField(payload.channel),
348
- ...payloadRefs(payload).flatMap((ref) => {
349
- const metadata = ref.metadata && typeof ref.metadata === 'object' && !Array.isArray(ref.metadata)
350
- ? ref.metadata
351
- : {};
352
- return [
353
- textField(ref.channel),
354
- textField(ref.channelName),
355
- textField(metadata.channelName),
356
- ];
357
- }),
358
- ].filter(Boolean));
359
- }
360
- function pipelineText(value) {
361
- return typeof value === 'string' ? value : '';
362
- }
363
- function pipelineTime(value) {
364
- const time = typeof value === 'number' ? value : Date.parse(pipelineText(value));
365
- return Number.isFinite(time) ? new Date(time).toISOString() : '';
366
- }
367
- function relatedWorkCandidates(row, workItems) {
368
- const rows = Array.isArray(workItems) ? workItems : [];
369
- const channels = stimulusChannels(row);
370
- const query = stimulusText(row);
371
- const cutoff = row.occurredAt - 14 * 24 * 60 * 60 * 1000;
372
- const candidates = rows.flatMap((raw) => {
373
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
374
- return [];
375
- const item = raw;
376
- const title = pipelineText(item.title);
377
- const commitmentId = pipelineText(item.commitmentId);
378
- const changedAt = typeof item.updatedAt === 'number' ? item.updatedAt : Number.NaN;
379
- if (!title || !commitmentId || !Number.isFinite(changedAt) || changedAt < cutoff)
380
- return [];
381
- const sameChannel = channels.has(pipelineText(item.sourceChannel));
382
- const overlap = (0, work_actions_js_1.workListTitleTextScore)(query, title);
383
- if (!sameChannel && overlap === 0)
384
- return [];
385
- return [{ item, title, commitmentId, overlap, sameChannel, changedAt }];
386
- });
387
- return candidates
388
- .sort((a, b) => Number(b.sameChannel) - Number(a.sameChannel) ||
389
- b.overlap - a.overlap ||
390
- b.changedAt - a.changedAt)
391
- .slice(0, 5)
392
- .map(({ item, title, commitmentId }) => `${title} | ${pipelineText(item.stage) || '-'} | ${pipelineText(item.assignee) || '-'} | ${pipelineTime(item.updatedAt)} | ${commitmentId}`);
393
- }
394
- function oneLine(value) {
395
- return value.replace(/\s+/g, ' ').trim();
396
- }
397
- function guidanceLine(entry) {
398
- const when = entry.applies_when
399
- ? `applies when: ${oneLine(entry.applies_when)}`
400
- : `summary: ${oneLine(entry.summary)}`;
401
- return `${oneLine(entry.id)} | ${entry.kind} | ${oneLine(entry.topic)} | ${when}`;
402
- }
403
- function guidanceVersion(entry) {
404
- return `${String(entry.updated_at)}\0${entry.kind}\0${entry.status}`;
405
- }
406
- function activeGuidance(entries) {
407
- return entries
408
- .filter((entry) => entry.status === 'active')
409
- .slice()
410
- .sort((left, right) => left.id.localeCompare(right.id));
411
- }
412
- function renderGuidanceIndex(entries) {
413
- return [
414
- '<guidance-index>',
415
- ...activeGuidance(entries).map(guidanceLine),
416
- '</guidance-index>',
417
- ].join('\n');
418
- }
419
- function renderGuidanceDelta(entries, previous) {
420
- const changes = [];
421
- for (const entry of entries) {
422
- const before = previous.get(entry.id);
423
- const after = guidanceVersion(entry);
424
- if (before === after)
425
- continue;
426
- if (entry.status === 'active') {
427
- changes.push(`${before === undefined ? 'added' : 'revised'}: ${guidanceLine(entry)}`);
428
- }
429
- else if (before?.endsWith('\0active')) {
430
- changes.push(`retired: ${oneLine(entry.id)} | ${entry.kind} | ${oneLine(entry.topic)} | status: ${entry.status}`);
431
- }
432
- }
433
- return changes.length === 0
434
- ? ''
435
- : [
436
- '<guidance-delta>',
437
- ...changes.sort((left, right) => left.localeCompare(right)),
438
- '</guidance-delta>',
439
- ].join('\n');
440
- }
441
- /** The turn carries the stimulus plus the session index or guidance changes. */
442
- function assembledContent(row, sessionBlocks, liveSourceDelta, options, candidates = []) {
443
- return [
444
- {
445
- type: 'text',
446
- text: [...sessionBlocks, boundedStimulus(row, liveSourceDelta, options, candidates)].join('\n\n'),
447
- },
448
- ];
449
- }
450
277
  /** One bounded log line; preserve the original thrown error for runtime settlement. */
451
278
  function stimulusFailureReason(error) {
452
279
  return (error instanceof Error ? error.message : String(error))
@@ -472,21 +299,115 @@ function replaySourceCeiling(row) {
472
299
  }
473
300
  return Number(replay.windowEndMs) - 1;
474
301
  }
302
+ function isRecordOrderRow(row) {
303
+ return row.kind === 'scheduled' && row.channelKey === turn_orders_js_1.RECORD_ORDER_CHANNEL;
304
+ }
305
+ function ownerText(row) {
306
+ const payload = row.payload;
307
+ return payload &&
308
+ typeof payload === 'object' &&
309
+ !Array.isArray(payload) &&
310
+ typeof payload.text === 'string'
311
+ ? payload.text
312
+ : '';
313
+ }
314
+ /** What a row's turn says, by kind; a live delta with only history lines has no turn. */
315
+ function planTurn(row, options) {
316
+ const zone = options.timeZone.get();
317
+ if (row.kind === 'owner_message') {
318
+ return {
319
+ kind: 'turn',
320
+ lessonQuery: ownerText(row),
321
+ render: (lessons) => (0, turn_orders_js_1.ownerMessageOrder)({
322
+ messenger: row.stimulusId.split(':', 1)[0],
323
+ occurredAt: row.occurredAt,
324
+ payload: row.payload,
325
+ }, lessons, { timeZone: zone }),
326
+ };
327
+ }
328
+ if (row.kind === 'source_delta') {
329
+ if (replaySourceCeiling(row) !== undefined)
330
+ return { kind: 'turn', lessonQuery: null, render: () => replayWindowText(row, options) };
331
+ // Kagemusha filters at collection: the reference is when the row was accepted, so a delta
332
+ // that waited behind a backlog is still delivered.
333
+ const all = (0, turn_orders_js_1.deltaLines)(row.payload);
334
+ const lines = (0, turn_orders_js_1.liveDeltaLines)(all, row.createdAt);
335
+ if (lines.length === 0)
336
+ return {
337
+ kind: 'skip',
338
+ reason: `all ${all.length} lines were older than the six-hour backfill guard when accepted`,
339
+ };
340
+ return {
341
+ kind: 'turn',
342
+ lessonQuery: lines.map((line) => line.text).join(' '),
343
+ render: (lessons, now) => (0, turn_orders_js_1.deltaNotifyOrder)(lines, row.channelKey, now, lessons, { timeZone: zone }),
344
+ };
345
+ }
346
+ if (row.kind === 'scheduled') {
347
+ if (row.channelKey === turn_orders_js_1.RECORD_ORDER_CHANNEL) {
348
+ const record = (0, turn_orders_js_1.parseRecordOrder)(row.payload);
349
+ return {
350
+ kind: 'turn',
351
+ lessonQuery: null,
352
+ render: (_lessons, now) => (0, turn_orders_js_1.deltaRecordOrder)(record, now, {
353
+ backend: options.backend,
354
+ timeZone: zone,
355
+ wikiEnabled: options.wikiEnabled ?? false,
356
+ }),
357
+ };
358
+ }
359
+ if (row.channelKey === turn_orders_js_1.REPORT_CHANNEL)
360
+ return {
361
+ kind: 'turn',
362
+ lessonQuery: null,
363
+ render: () => (0, turn_orders_js_1.scheduledReportOrder)(row.payload, new Date(row.occurredAt), {
364
+ backend: options.backend,
365
+ timeZone: zone,
366
+ messenger: options.formattingRoutes?.reports ?? 'telegram',
367
+ }),
368
+ };
369
+ throw new Error(`Scheduled input on unknown channel ${row.channelKey}`);
370
+ }
371
+ throw new Error(`No owner turn is assembled for stimulus kind ${row.kind ?? 'unknown'}`);
372
+ }
475
373
  /** Deliver every model-bearing kind through one serialized owner session. */
476
374
  function createStimulusDelivery(options) {
477
375
  let serialTail = Promise.resolve();
478
376
  let activeReplaySourceEndMs;
479
- const guidanceBySessionKey = new Map();
377
+ // Lessons shown in this session, cleared on a new session or a new local day: compactions are
378
+ // not observable, so a day bounds how long a lesson stays out of view.
379
+ let shownLessons = new Set();
380
+ let shownDay = '';
381
+ const pickLessons = async (query, newSession) => {
382
+ const day = (0, timezone_js_1.localDateKey)(Date.now(), options.timeZone.get());
383
+ if (newSession || day !== shownDay) {
384
+ shownLessons = new Set();
385
+ shownDay = day;
386
+ }
387
+ if (query === null || query.trim() === '' || !options.lessons)
388
+ return [];
389
+ const fresh = (await options.lessons(query))
390
+ .filter((lesson) => !shownLessons.has(lesson.id))
391
+ .slice(0, 3);
392
+ for (const lesson of fresh)
393
+ shownLessons.add(lesson.id);
394
+ return fresh;
395
+ };
480
396
  const deliverResult = async (row, result, modelRunId = result.modelRunId) => {
481
397
  if (row.kind === 'owner_message')
482
398
  await options.onOwnerResult?.(row, result);
483
- if (row.kind === 'source_delta' && replaySourceCeiling(row) === undefined)
399
+ if (row.kind === 'source_delta' && replaySourceCeiling(row) === undefined) {
400
+ // The record order is durable before the notify reply goes anywhere.
401
+ options.recordOrders?.enqueueFirst(row);
484
402
  await options.onSourceResult?.(row, result);
485
- if (row.kind === 'scheduled')
486
- await options.onScheduledResult?.(row, result);
487
- if (row.kind === 'native_event')
488
- await options.onNativeEventResult?.(row, result);
489
- await options.onDelivered?.(row, modelRunId);
403
+ }
404
+ if (row.kind === 'scheduled') {
405
+ if (isRecordOrderRow(row))
406
+ await options.recordOrders?.onResult(row, modelRunId ?? null);
407
+ else
408
+ await options.onScheduledResult?.(row, result);
409
+ }
410
+ await options.onDelivered?.(row, modelRunId ?? null);
490
411
  };
491
412
  const deliver = async (row, context) => {
492
413
  let release;
@@ -496,46 +417,32 @@ function createStimulusDelivery(options) {
496
417
  });
497
418
  await previous;
498
419
  let modelRunId = null;
499
- let pendingGuidanceState;
500
420
  try {
501
421
  const replaySourceEndMs = replaySourceCeiling(row);
502
422
  activeReplaySourceEndMs = replaySourceEndMs;
503
- const liveSourceDelta = row.kind === 'source_delta' && replaySourceEndMs === undefined;
504
- if (row.kind !== 'owner_message' &&
505
- row.kind !== 'source_delta' &&
506
- row.kind !== 'scheduled' &&
507
- row.kind !== 'native_event') {
508
- throw new Error('Stimulus kind is missing; no owner turn can be assembled');
423
+ const plan = planTurn(row, options);
424
+ if (plan.kind === 'skip') {
425
+ // Kagemusha's backfill guard: only history lines, so no live turn and no record order.
426
+ await options.onSkipped?.(row, plan.reason);
427
+ await options.onDelivered?.(row, null);
428
+ return;
509
429
  }
510
- const result = await context.run(assembledContent(row, [], liveSourceDelta, options), {
430
+ const text = (blocks) => [
431
+ { type: 'text', text: blocks.filter((block) => block !== '').join('\n\n') },
432
+ ];
433
+ const result = await context.run(text([plan.render([], new Date())]), {
511
434
  onModelRunStarted: (id) => {
512
435
  modelRunId = id;
513
436
  },
514
437
  prepareSessionContent: async ({ isNewSession }) => {
515
- const sessionBlocks = [];
516
- const pipeline = isNewSession ? await options.openWorkPipeline?.() : undefined;
517
- const entries = await options.guidanceResolver();
518
- const sessionKey = exports.OWNER_RUNTIME_SESSION_KEY;
519
- const lastDelivered = guidanceBySessionKey.get(sessionKey);
520
- if (isNewSession) {
521
- sessionBlocks.push(renderGuidanceIndex(entries));
522
- if (pipeline !== undefined) {
523
- sessionBlocks.push(`<open-work-pipeline>\n${JSON.stringify(pipeline)}\n</open-work-pipeline>`);
524
- }
525
- const exchanges = (0, recent_owner_exchanges_js_1.renderRecentOwnerExchanges)((await options.recentOwnerExchanges?.(row)) ?? []);
526
- if (exchanges)
527
- sessionBlocks.push(exchanges);
528
- }
529
- else if (lastDelivered === undefined) {
530
- sessionBlocks.push(renderGuidanceIndex(entries));
531
- }
532
- else {
533
- const delta = renderGuidanceDelta(entries, lastDelivered);
534
- if (delta)
535
- sessionBlocks.push(delta);
536
- }
537
- pendingGuidanceState = new Map(entries.map((entry) => [entry.id, guidanceVersion(entry)]));
538
- return assembledContent(row, sessionBlocks, liveSourceDelta, options, liveSourceDelta ? relatedWorkCandidates(row, await options.openWorkCandidates?.()) : []);
438
+ const lessons = await pickLessons(plan.lessonQuery, isNewSession);
439
+ const start = isNewSession
440
+ ? (0, turn_orders_js_1.sessionStartBlock)((await options.recentOwnerExchanges?.(row)) ?? [], new Date(), {
441
+ backend: options.backend,
442
+ timeZone: options.timeZone.get(),
443
+ })
444
+ : '';
445
+ return text([start, plan.render(lessons, new Date())]);
539
446
  },
540
447
  sessionKey: exports.OWNER_RUNTIME_SESSION_KEY,
541
448
  source: row.kind,
@@ -543,9 +450,6 @@ function createStimulusDelivery(options) {
543
450
  sourceMessageRef: row.stimulusId,
544
451
  ...(replaySourceEndMs === undefined ? {} : { replaySourceEndMs }),
545
452
  });
546
- if (pendingGuidanceState !== undefined) {
547
- guidanceBySessionKey.set(exports.OWNER_RUNTIME_SESSION_KEY, pendingGuidanceState);
548
- }
549
453
  // A commit failure withholds result provenance, but the opened run still identifies this turn.
550
454
  modelRunId = result.modelRunId ?? modelRunId;
551
455
  await deliverResult(row, result, modelRunId);
@@ -559,10 +463,21 @@ function createStimulusDelivery(options) {
559
463
  release();
560
464
  }
561
465
  };
466
+ const lost = async (row, reason) => {
467
+ if (isRecordOrderRow(row))
468
+ await options.recordOrders?.onLost(row, reason);
469
+ };
562
470
  return {
563
471
  deliver,
564
472
  prefer: ['owner_message'],
565
- ...(options.onUncertain === undefined ? {} : { onUncertain: options.onUncertain }),
473
+ onUncertain: async (row, reason) => {
474
+ await lost(row, reason);
475
+ await options.onUncertain?.(row, reason);
476
+ },
477
+ onDead: async (row, reason) => {
478
+ await lost(row, reason);
479
+ await options.onDead?.(row, reason);
480
+ },
566
481
  reconcile: async (row) => {
567
482
  const result = options.readResult?.(row);
568
483
  if (result) {
@@ -572,9 +487,11 @@ function createStimulusDelivery(options) {
572
487
  // Core invokes reconciliation only when no delivery in this process owns
573
488
  // the input. Never rerun an orphan dispatched to a model without its final result.
574
489
  if (row.nativeDelivery?.state === 'dispatching' || row.nativeDelivery?.state === 'accepted') {
575
- const reason = row.kind === 'scheduled'
576
- ? 'Scheduled report interrupted before completion; retry on the next report tick'
577
- : `${row.kind} interrupted before completion; no stored result`;
490
+ const reason = isRecordOrderRow(row)
491
+ ? 'Record order interrupted before completion; the ledger check decides the next attempt'
492
+ : row.kind === 'scheduled'
493
+ ? 'Scheduled report interrupted before completion; retry on the next report tick'
494
+ : `${row.kind} interrupted before completion; no stored result`;
578
495
  await options.onFailed?.(row, reason, null);
579
496
  // Core parks the orphan uncertain, preserving its receipt and any result.
580
497
  throw new Error(reason);