@jungjaehoon/mama-os 0.32.2 → 0.33.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.
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +58 -24
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/runtime/api-routes-init.d.ts +2 -0
- package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
- package/dist/cli/runtime/api-routes-init.js +61 -0
- package/dist/cli/runtime/api-routes-init.js.map +1 -1
- package/dist/gateways/message-router.d.ts +4 -0
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +62 -13
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/gateways/owner-report-inbox.d.ts +40 -0
- package/dist/gateways/owner-report-inbox.d.ts.map +1 -0
- package/dist/gateways/owner-report-inbox.js +128 -0
- package/dist/gateways/owner-report-inbox.js.map +1 -0
- package/dist/gateways/report-projection-v1.d.ts +41 -0
- package/dist/gateways/report-projection-v1.d.ts.map +1 -0
- package/dist/gateways/report-projection-v1.js +160 -0
- package/dist/gateways/report-projection-v1.js.map +1 -0
- package/dist/gateways/session-store.d.ts +22 -0
- package/dist/gateways/session-store.d.ts.map +1 -1
- package/dist/gateways/session-store.js +76 -3
- package/dist/gateways/session-store.js.map +1 -1
- package/dist/gateways/telegram-message-ledger.d.ts +10 -0
- package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
- package/dist/gateways/telegram-message-ledger.js +23 -2
- package/dist/gateways/telegram-message-ledger.js.map +1 -1
- package/dist/gateways/telegram-report-context-store.d.ts +193 -0
- package/dist/gateways/telegram-report-context-store.d.ts.map +1 -0
- package/dist/gateways/telegram-report-context-store.js +612 -0
- package/dist/gateways/telegram-report-context-store.js.map +1 -0
- package/dist/gateways/telegram.d.ts +9 -0
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +100 -0
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/gateways/types.d.ts +12 -0
- package/dist/gateways/types.d.ts.map +1 -1
- package/dist/operator/operator-trigger-loop.d.ts +39 -7
- package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
- package/dist/operator/operator-trigger-loop.js +282 -89
- package/dist/operator/operator-trigger-loop.js.map +1 -1
- package/dist/operator/report-delivery-coordinator.d.ts +82 -0
- package/dist/operator/report-delivery-coordinator.d.ts.map +1 -0
- package/dist/operator/report-delivery-coordinator.js +123 -0
- package/dist/operator/report-delivery-coordinator.js.map +1 -0
- package/dist/operator/situation-report.d.ts +7 -0
- package/dist/operator/situation-report.d.ts.map +1 -1
- package/dist/operator/situation-report.js +12 -0
- package/dist/operator/situation-report.js.map +1 -1
- package/dist/operator/trigger-author.d.ts +10 -2
- package/dist/operator/trigger-author.d.ts.map +1 -1
- package/dist/operator/trigger-author.js +112 -39
- package/dist/operator/trigger-author.js.map +1 -1
- package/dist/operator/trigger-registry.d.ts +21 -0
- package/dist/operator/trigger-registry.d.ts.map +1 -1
- package/dist/operator/trigger-registry.js +161 -1
- package/dist/operator/trigger-registry.js.map +1 -1
- package/dist/operator/trigger-review.d.ts.map +1 -1
- package/dist/operator/trigger-review.js +56 -10
- package/dist/operator/trigger-review.js.map +1 -1
- package/package.json +1 -1
|
@@ -27,8 +27,15 @@ const leg_cadence_js_1 = require("./leg-cadence.js");
|
|
|
27
27
|
class OperatorTriggerLoop {
|
|
28
28
|
deps;
|
|
29
29
|
tickCount = 0;
|
|
30
|
+
maintenanceTickCount = 0;
|
|
31
|
+
authorWindowGeneration = 0;
|
|
32
|
+
authoredWindowGeneration = 0;
|
|
30
33
|
recentEvents = [];
|
|
31
34
|
running = false;
|
|
35
|
+
maintenancePending = false;
|
|
36
|
+
schedulerActive = false;
|
|
37
|
+
stopping = false;
|
|
38
|
+
activeRunPromise = null;
|
|
32
39
|
nudgeTimer = null;
|
|
33
40
|
digest;
|
|
34
41
|
fullReporter;
|
|
@@ -37,6 +44,14 @@ class OperatorTriggerLoop {
|
|
|
37
44
|
pendingReportExpectation;
|
|
38
45
|
pendingReportLegacyLoaded = false;
|
|
39
46
|
constructor(deps) {
|
|
47
|
+
// Design Decision 1: exactly one delivery executor. The legacy OutputSink
|
|
48
|
+
// report path cannot run beside the coordinator port.
|
|
49
|
+
if (deps.reportDelivery && deps.output) {
|
|
50
|
+
throw new Error('OperatorTriggerLoop: legacy report output cannot run beside ReportDeliveryPort');
|
|
51
|
+
}
|
|
52
|
+
if (deps.reportDelivery && !deps.reportTarget) {
|
|
53
|
+
throw new Error('OperatorTriggerLoop: reportDelivery requires reportTarget');
|
|
54
|
+
}
|
|
40
55
|
this.deps = deps;
|
|
41
56
|
// G2 success signal: when a sent report cites fired triggers (USED_TRIGGERS
|
|
42
57
|
// trailer, window-validated), record 'succeeded' on each. Uncited fires stay
|
|
@@ -75,7 +90,7 @@ class OperatorTriggerLoop {
|
|
|
75
90
|
}
|
|
76
91
|
/** Reloads one durable outcome so recovery can hydrate this live loop before report work. */
|
|
77
92
|
refreshPendingReportState() {
|
|
78
|
-
if (!this.
|
|
93
|
+
if (!this.hasReportSink()) {
|
|
79
94
|
return 'unavailable';
|
|
80
95
|
}
|
|
81
96
|
const store = this.deps.pendingReportStore;
|
|
@@ -120,7 +135,7 @@ class OperatorTriggerLoop {
|
|
|
120
135
|
return this.refreshPendingReportState() === 'quarantined';
|
|
121
136
|
}
|
|
122
137
|
persistPendingReports() {
|
|
123
|
-
if (!this.
|
|
138
|
+
if (!this.hasReportSink())
|
|
124
139
|
return false;
|
|
125
140
|
if (this.refreshPendingReportState() === 'quarantined')
|
|
126
141
|
return false;
|
|
@@ -144,6 +159,10 @@ class OperatorTriggerLoop {
|
|
|
144
159
|
reporterFor(mode) {
|
|
145
160
|
return mode === 'full' ? this.fullReporter : this.digest;
|
|
146
161
|
}
|
|
162
|
+
/** A report sink exists: either the coordinator port or the legacy test sink. */
|
|
163
|
+
hasReportSink() {
|
|
164
|
+
return Boolean(this.deps.reportDelivery ?? this.deps.output);
|
|
165
|
+
}
|
|
147
166
|
deliveryIdFor(occurrence) {
|
|
148
167
|
if (occurrence.kind === 'scheduled_full' && occurrence.hourKey) {
|
|
149
168
|
return `operator-report:scheduled:${occurrence.hourKey}`;
|
|
@@ -151,7 +170,7 @@ class OperatorTriggerLoop {
|
|
|
151
170
|
return `operator-report:${occurrence.kind}:${(0, node_crypto_1.randomUUID)()}`;
|
|
152
171
|
}
|
|
153
172
|
requireOutputTarget() {
|
|
154
|
-
const target = this.deps.output?.target;
|
|
173
|
+
const target = this.deps.reportTarget ?? this.deps.output?.target;
|
|
155
174
|
if (target?.source !== 'telegram' ||
|
|
156
175
|
target.channelId.length === 0 ||
|
|
157
176
|
target.channelId.trim() !== target.channelId) {
|
|
@@ -181,20 +200,56 @@ class OperatorTriggerLoop {
|
|
|
181
200
|
}
|
|
182
201
|
async deliverPendingReport(recovered) {
|
|
183
202
|
const pending = this.pendingDelivery;
|
|
184
|
-
|
|
185
|
-
if (!pending || !output) {
|
|
203
|
+
if (!pending || !this.hasReportSink()) {
|
|
186
204
|
return null;
|
|
187
205
|
}
|
|
188
206
|
this.assertPendingDeliveryBinding(pending);
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
const port = this.deps.reportDelivery;
|
|
208
|
+
if (port) {
|
|
209
|
+
// TG-05/TG-06: submit the persisted artifact and observe the typed
|
|
210
|
+
// outcome. Only `delivered` advances scheduler success, trigger credit,
|
|
211
|
+
// and pending-file cleanup; retry/rejection retain the artifact for the
|
|
212
|
+
// coordinator to drive to terminal; cancellation cleans up with an
|
|
213
|
+
// audit record and no credit.
|
|
214
|
+
const outcome = await port.deliverPrepared(pending);
|
|
215
|
+
if (outcome.status === 'delivered') {
|
|
216
|
+
this.reporterFor(pending.mode).markDeliveredOutcome(pending.citedTriggerIds);
|
|
217
|
+
this.markScheduledFullOutcome(pending);
|
|
218
|
+
this.pendingDelivery = undefined;
|
|
219
|
+
this.persistPendingReports();
|
|
220
|
+
if (recovered) {
|
|
221
|
+
this.deps.log(`[trigger-loop] recovered pending ${pending.mode} owner report delivery=${pending.deliveryId}`);
|
|
222
|
+
}
|
|
223
|
+
return pending;
|
|
193
224
|
}
|
|
194
|
-
if (
|
|
195
|
-
this.
|
|
225
|
+
if (outcome.status === 'cancelled') {
|
|
226
|
+
this.pendingDelivery = undefined;
|
|
227
|
+
this.persistPendingReports();
|
|
228
|
+
this.deps.log(`[trigger-loop] owner report delivery=${pending.deliveryId} cancelled without credit: ${outcome.reason}`);
|
|
229
|
+
return null;
|
|
196
230
|
}
|
|
231
|
+
if (outcome.status === 'capacity_full') {
|
|
232
|
+
// Loud degradation, never a wedge: the durable artifact is retained,
|
|
233
|
+
// the tick continues, and recovery only needs consumption or an
|
|
234
|
+
// explicit operator archive to reopen capacity.
|
|
235
|
+
this.persistPendingReports();
|
|
236
|
+
this.deps.log(`[trigger-loop] owner report delivery=${pending.deliveryId} BLOCKED by report-context capacity: ${outcome.reason}`);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
this.persistPendingReports();
|
|
240
|
+
this.deps.log(outcome.status === 'retry_scheduled'
|
|
241
|
+
? `[trigger-loop] owner report delivery=${pending.deliveryId} retry scheduled at ${outcome.nextAttemptAt}`
|
|
242
|
+
: `[trigger-loop] owner report delivery=${pending.deliveryId} definitively rejected: ${outcome.reason}`);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
// LEGACY OutputSink path - V2 compatibility tests only; the constructor
|
|
246
|
+
// guard keeps it out of any assembly that wires the coordinator port.
|
|
247
|
+
const output = this.deps.output;
|
|
248
|
+
if (!output) {
|
|
249
|
+
return null;
|
|
197
250
|
}
|
|
251
|
+
await this.reporterFor(pending.mode).deliverPrepared(pending, output);
|
|
252
|
+
this.markScheduledFullOutcome(pending);
|
|
198
253
|
this.pendingDelivery = undefined;
|
|
199
254
|
this.persistPendingReports();
|
|
200
255
|
if (recovered) {
|
|
@@ -202,8 +257,19 @@ class OperatorTriggerLoop {
|
|
|
202
257
|
}
|
|
203
258
|
return pending;
|
|
204
259
|
}
|
|
260
|
+
markScheduledFullOutcome(pending) {
|
|
261
|
+
if (pending.mode !== 'full' || !this.deps.reportScheduler) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (pending.occurrence.hourKey) {
|
|
265
|
+
this.deps.reportScheduler.markFired(pending.occurrence.hourKey);
|
|
266
|
+
}
|
|
267
|
+
if (pending.occurrence.firedAtIso) {
|
|
268
|
+
this.deps.reportScheduler.markSuccess(pending.occurrence.firedAtIso);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
205
271
|
async prepareAndDeliverReport(askAgent, mode, occurrence) {
|
|
206
|
-
if (!this.
|
|
272
|
+
if (!this.hasReportSink() || this.isPendingReportWorkBlocked()) {
|
|
207
273
|
return false;
|
|
208
274
|
}
|
|
209
275
|
if (this.pendingDelivery) {
|
|
@@ -236,7 +302,7 @@ class OperatorTriggerLoop {
|
|
|
236
302
|
}
|
|
237
303
|
async preparePendingRequest() {
|
|
238
304
|
const request = this.pendingRequest;
|
|
239
|
-
if (!request || !this.
|
|
305
|
+
if (!request || !this.hasReportSink() || this.isPendingReportWorkBlocked())
|
|
240
306
|
return false;
|
|
241
307
|
if (this.pendingDelivery) {
|
|
242
308
|
throw new Error('A pending owner report delivery must be recovered before its request');
|
|
@@ -278,42 +344,61 @@ class OperatorTriggerLoop {
|
|
|
278
344
|
}
|
|
279
345
|
return recovered;
|
|
280
346
|
}
|
|
281
|
-
async tick() {
|
|
347
|
+
async tick(options = {}) {
|
|
282
348
|
const { delta, memory, registry, askAgent, review, config, log } = this.deps;
|
|
283
|
-
const {
|
|
284
|
-
const
|
|
349
|
+
const { reportScheduler } = this.deps;
|
|
350
|
+
const reportSink = this.hasReportSink();
|
|
351
|
+
const fullLegOn = Boolean(reportSink && reportScheduler);
|
|
285
352
|
this.tickCount += 1;
|
|
286
353
|
const tick = this.tickCount;
|
|
354
|
+
const maintenanceTick = options.advanceMaintenance === false ? null : (this.maintenanceTickCount += 1);
|
|
355
|
+
let fires = 0;
|
|
356
|
+
let authored = 0;
|
|
357
|
+
let reviewed = 0;
|
|
358
|
+
let reported = false;
|
|
359
|
+
let fullReported = false;
|
|
360
|
+
const result = (drained = 0) => ({
|
|
361
|
+
tick,
|
|
362
|
+
drained,
|
|
363
|
+
fires,
|
|
364
|
+
authored,
|
|
365
|
+
reviewed,
|
|
366
|
+
reported,
|
|
367
|
+
fullReported,
|
|
368
|
+
});
|
|
287
369
|
// Outbox recovery is the first effect in a tick. It reuses the persisted
|
|
288
370
|
// delivery id, allowing Telegram's confirmed-chunk ledger to suppress a
|
|
289
371
|
// send that was accepted just before the prior daemon stopped.
|
|
290
372
|
this.refreshPendingReportState();
|
|
291
373
|
const recoveredPendingWork = await this.recoverPendingReportWork();
|
|
374
|
+
if (this.stopping)
|
|
375
|
+
return result();
|
|
292
376
|
// 1. Drain new deltas (commit AFTER processing - at-least-once).
|
|
293
377
|
const events = delta.drainNew(config.drainLimit);
|
|
294
|
-
const reportEvents =
|
|
378
|
+
const reportEvents = reportSink
|
|
295
379
|
? events.filter((event) => !this.digest.hasRecordedEvent(event))
|
|
296
380
|
: events;
|
|
297
381
|
if (events.length > 0) {
|
|
298
382
|
log(`[trigger-loop] tick ${tick}: drained ${events.length} events`);
|
|
299
383
|
}
|
|
300
384
|
// 2. Match + fire + recordFire, folding fire activity into the report accumulators.
|
|
301
|
-
let fires = 0;
|
|
302
385
|
for (const event of reportEvents) {
|
|
303
386
|
const signals = (0, trigger_matcher_js_1.matchTriggers)(event, registry);
|
|
304
387
|
for (const signal of signals) {
|
|
305
|
-
const
|
|
388
|
+
const fireResult = await (0, trigger_fire_js_1.fireTrigger)(signal, memory);
|
|
389
|
+
if (this.stopping)
|
|
390
|
+
return result(events.length);
|
|
306
391
|
fires += 1;
|
|
307
392
|
if (signal.triggerId) {
|
|
308
393
|
registry.recordFire(signal.triggerId);
|
|
309
394
|
}
|
|
310
395
|
// Carry the recalled {topic, content} (agent-authored memoryQuery drove it) into the report.
|
|
311
|
-
if (
|
|
396
|
+
if (reportSink) {
|
|
312
397
|
this.digest.recordFire({
|
|
313
398
|
triggerId: signal.triggerId ?? signal.detector,
|
|
314
399
|
kind: signal.kind,
|
|
315
400
|
channelId: signal.channelId,
|
|
316
|
-
recalled:
|
|
401
|
+
recalled: fireResult.recalled,
|
|
317
402
|
});
|
|
318
403
|
}
|
|
319
404
|
if (fullLegOn) {
|
|
@@ -321,23 +406,24 @@ class OperatorTriggerLoop {
|
|
|
321
406
|
triggerId: signal.triggerId ?? signal.detector,
|
|
322
407
|
kind: signal.kind,
|
|
323
408
|
channelId: signal.channelId,
|
|
324
|
-
recalled:
|
|
409
|
+
recalled: fireResult.recalled,
|
|
325
410
|
});
|
|
326
411
|
}
|
|
327
412
|
log(`[trigger-loop] tick ${tick}: fire trigger=${signal.triggerId ?? signal.detector} ` +
|
|
328
|
-
`recalled=${
|
|
413
|
+
`recalled=${fireResult.recalled.length} channel=${signal.channelId}`);
|
|
329
414
|
}
|
|
330
415
|
}
|
|
331
416
|
// Persist the report window BEFORE advancing the connector cursor. A daemon
|
|
332
417
|
// crash may repeat an event, but it cannot silently lose an owner update.
|
|
333
418
|
if (reportEvents.length > 0) {
|
|
334
|
-
if (
|
|
419
|
+
if (reportSink) {
|
|
335
420
|
this.digest.recordWindow(reportEvents);
|
|
336
421
|
if (fullLegOn) {
|
|
337
422
|
this.fullReporter.recordWindow(reportEvents);
|
|
338
423
|
}
|
|
339
424
|
}
|
|
340
425
|
this.recentEvents = [...this.recentEvents, ...reportEvents].slice(-config.authorWindowSize);
|
|
426
|
+
this.authorWindowGeneration += 1;
|
|
341
427
|
this.persistPendingReports();
|
|
342
428
|
}
|
|
343
429
|
// Group per channel ONCE, before the cursor moves: the conductor inbox
|
|
@@ -404,46 +490,96 @@ class OperatorTriggerLoop {
|
|
|
404
490
|
}
|
|
405
491
|
}
|
|
406
492
|
// 3. Agent authors new triggers from the recent window.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
493
|
+
if (maintenanceTick !== null &&
|
|
494
|
+
maintenanceTick % config.authorEveryNTicks === 0 &&
|
|
495
|
+
this.recentEvents.length > 0 &&
|
|
496
|
+
this.authorWindowGeneration > this.authoredWindowGeneration) {
|
|
497
|
+
if (registry.canAttemptAuthor()) {
|
|
498
|
+
try {
|
|
499
|
+
const created = await (0, trigger_author_js_1.authorTriggers)(this.recentEvents, registry, askAgent, {
|
|
500
|
+
note: `authored at tick ${tick}`,
|
|
501
|
+
});
|
|
502
|
+
if (this.stopping)
|
|
503
|
+
return result(events.length);
|
|
504
|
+
authored = created.length;
|
|
505
|
+
registry.clearAuthorFailure();
|
|
506
|
+
this.authoredWindowGeneration = this.authorWindowGeneration;
|
|
507
|
+
if (authored > 0) {
|
|
508
|
+
if (reportSink) {
|
|
509
|
+
this.digest.recordAuthored(authored);
|
|
510
|
+
}
|
|
511
|
+
if (fullLegOn) {
|
|
512
|
+
this.fullReporter.recordAuthored(authored);
|
|
513
|
+
}
|
|
514
|
+
this.persistPendingReports();
|
|
515
|
+
}
|
|
516
|
+
log(`[trigger-loop] tick ${tick}: author pass created ${authored} trigger(s)`);
|
|
416
517
|
}
|
|
417
|
-
|
|
418
|
-
this.
|
|
518
|
+
catch (error) {
|
|
519
|
+
if (this.stopping) {
|
|
520
|
+
log(`[trigger-loop] tick ${tick}: author pass cancelled by shutdown`);
|
|
521
|
+
return result(events.length);
|
|
522
|
+
}
|
|
523
|
+
registry.recordAuthorFailure(authorWindowFingerprint(this.recentEvents));
|
|
524
|
+
log(`[trigger-loop] tick ${tick}: author pass FAILED; backed off: ${error instanceof Error ? error.message : String(error)}`);
|
|
419
525
|
}
|
|
420
|
-
this.persistPendingReports();
|
|
421
526
|
}
|
|
422
|
-
log(`[trigger-loop] tick ${tick}: author pass created ${authored} trigger(s)`);
|
|
423
527
|
}
|
|
424
528
|
// 4. Agent reviews triggers that have actually fired.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
529
|
+
if (this.stopping)
|
|
530
|
+
return result(events.length);
|
|
531
|
+
if (maintenanceTick !== null && maintenanceTick % config.reviewEveryNTicks === 0) {
|
|
532
|
+
const configuredReviewLimit = config.reviewBatchLimit;
|
|
533
|
+
const reviewBatchLimit = typeof configuredReviewLimit === 'number' &&
|
|
534
|
+
Number.isInteger(configuredReviewLimit) &&
|
|
535
|
+
configuredReviewLimit > 0
|
|
536
|
+
? configuredReviewLimit
|
|
537
|
+
: 1;
|
|
538
|
+
const firedTriggers = registry.listReviewCandidates(reviewBatchLimit);
|
|
539
|
+
const context = boundedReviewContext(this.recentEvents);
|
|
429
540
|
for (const trigger of firedTriggers) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
541
|
+
try {
|
|
542
|
+
const decision = await review(trigger, context);
|
|
543
|
+
if (this.stopping)
|
|
544
|
+
return result(events.length);
|
|
545
|
+
const action = (0, trigger_review_js_1.applyReview)(decision, trigger.id, registry);
|
|
546
|
+
if (action === 'kept') {
|
|
547
|
+
registry.markReviewed(trigger.id, trigger.stats.fired);
|
|
548
|
+
}
|
|
549
|
+
reviewed += 1;
|
|
550
|
+
log(`[trigger-loop] tick ${tick}: review trigger=${trigger.id} -> ${action}`);
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
if (this.stopping) {
|
|
554
|
+
log(`[trigger-loop] tick ${tick}: review trigger=${trigger.id} cancelled by shutdown`);
|
|
555
|
+
return result(events.length);
|
|
556
|
+
}
|
|
557
|
+
let backoffNote = 'backed off';
|
|
558
|
+
try {
|
|
559
|
+
registry.recordReviewFailure(trigger.id);
|
|
560
|
+
}
|
|
561
|
+
catch (stateError) {
|
|
562
|
+
backoffNote = `state changed before backoff: ${stateError instanceof Error ? stateError.message : String(stateError)}`;
|
|
563
|
+
}
|
|
564
|
+
log(`[trigger-loop] tick ${tick}: review trigger=${trigger.id} FAILED; ${backoffNote}: ${error instanceof Error ? error.message : String(error)}`);
|
|
565
|
+
}
|
|
434
566
|
}
|
|
435
567
|
}
|
|
436
568
|
// 5. Situational digest (M1.5 cadence, M2 window-aware): the agent composes it from the
|
|
437
569
|
// window + fire activity + recalled memory; the sink delivers it. Agent may reply NOTHING.
|
|
438
|
-
|
|
570
|
+
if (this.stopping)
|
|
571
|
+
return result(events.length);
|
|
439
572
|
const reportAsk = this.deps.reportAsk ?? askAgent;
|
|
440
573
|
const reportEvery = config.reportEveryNTicks ?? 0;
|
|
441
574
|
if (!this.isPendingReportWorkBlocked() &&
|
|
442
|
-
|
|
575
|
+
!this.pendingDelivery &&
|
|
576
|
+
reportSink &&
|
|
443
577
|
reportEvery > 0 &&
|
|
444
578
|
tick % reportEvery === 0 &&
|
|
445
579
|
this.digest.hasActivity()) {
|
|
446
580
|
reported = await this.prepareAndDeliverReport(reportAsk, 'digest', { kind: 'digest' });
|
|
581
|
+
if (this.stopping)
|
|
582
|
+
return result(events.length);
|
|
447
583
|
log(`[trigger-loop] tick ${tick}: owner digest ${reported ? 'SENT' : 'suppressed by agent'}`);
|
|
448
584
|
}
|
|
449
585
|
// 6. Scheduled full report (M2): fires at configured LOCAL hours - even on a completely
|
|
@@ -451,8 +587,15 @@ class OperatorTriggerLoop {
|
|
|
451
587
|
// rely on the scheduled report arriving). Fires once per hour (markFired persists the
|
|
452
588
|
// hour key -> restart-safe). Send failure throws (no-fallback) WITHOUT marking the hour,
|
|
453
589
|
// so the next tick retries with the buffer intact.
|
|
454
|
-
|
|
455
|
-
|
|
590
|
+
if (!this.stopping &&
|
|
591
|
+
!recoveredPendingWork &&
|
|
592
|
+
!this.isPendingReportWorkBlocked() &&
|
|
593
|
+
// An outstanding delivery (retry backoff or definite rejection) parks
|
|
594
|
+
// the scheduled leg WITHOUT consuming the hour: the coordinator drives
|
|
595
|
+
// the pending artifact to terminal, then the next due tick fires.
|
|
596
|
+
!this.pendingDelivery &&
|
|
597
|
+
reportSink &&
|
|
598
|
+
reportScheduler) {
|
|
456
599
|
const { fire, hourKey } = reportScheduler.shouldFire(new Date());
|
|
457
600
|
if (fire) {
|
|
458
601
|
// On-demand merge suppression (plan v6 S1-T3): an owner-requested full
|
|
@@ -478,11 +621,13 @@ class OperatorTriggerLoop {
|
|
|
478
621
|
hourKey,
|
|
479
622
|
firedAtIso,
|
|
480
623
|
});
|
|
624
|
+
if (this.stopping)
|
|
625
|
+
return result(events.length);
|
|
481
626
|
log(`[trigger-loop] tick ${tick}: full report ${fullReported ? 'SENT' : 'suppressed by agent'} (${hourKey})`);
|
|
482
627
|
}
|
|
483
628
|
}
|
|
484
629
|
}
|
|
485
|
-
return
|
|
630
|
+
return result(events.length);
|
|
486
631
|
}
|
|
487
632
|
/**
|
|
488
633
|
* M2.4 freshness nudge: wake the loop to tick ~nudgeDebounceMs from now instead of waiting for the
|
|
@@ -493,8 +638,8 @@ class OperatorTriggerLoop {
|
|
|
493
638
|
* a quiet window arms one timer; further nudges while it is armed are ignored, so a burst of poll
|
|
494
639
|
* batches collapses to a single extra tick. Busy-safe (agent-awareness.ts:343-346 mechanism): if a
|
|
495
640
|
* tick is in flight when the timer fires, the nudge is skipped - never concurrent ticks; the
|
|
496
|
-
* uncommitted deltas simply wait for the next tick.
|
|
497
|
-
*
|
|
641
|
+
* uncommitted deltas simply wait for the next tick. The extra tick drains and reports, but does
|
|
642
|
+
* not advance author/review maintenance cadence.
|
|
498
643
|
*/
|
|
499
644
|
/**
|
|
500
645
|
* On-demand full report (plan v6 S1-T3): the owner's "give me the full
|
|
@@ -508,9 +653,10 @@ class OperatorTriggerLoop {
|
|
|
508
653
|
* the delta anchor exactly like a scheduled run.
|
|
509
654
|
*/
|
|
510
655
|
startFullReport() {
|
|
511
|
-
|
|
656
|
+
if (this.stopping)
|
|
657
|
+
return { accepted: false, reason: 'unavailable' };
|
|
512
658
|
const reportScheduler = this.deps.reportScheduler;
|
|
513
|
-
if (!
|
|
659
|
+
if (!this.hasReportSink() || this.isPendingReportWorkBlocked()) {
|
|
514
660
|
return { accepted: false, reason: 'unavailable' };
|
|
515
661
|
}
|
|
516
662
|
if (this.running || this.pendingDelivery || this.pendingRequest) {
|
|
@@ -553,20 +699,14 @@ class OperatorTriggerLoop {
|
|
|
553
699
|
this.deps.log(`[trigger-loop] on-demand full report could not be accepted durably: ${error instanceof Error ? error.message : String(error)}`);
|
|
554
700
|
return { accepted: false, reason: 'unavailable' };
|
|
555
701
|
}
|
|
556
|
-
this.
|
|
557
|
-
void this.preparePendingRequest()
|
|
558
|
-
.then((sent) => {
|
|
702
|
+
this.launchRun(this.preparePendingRequest().then((sent) => {
|
|
559
703
|
this.deps.log(`[trigger-loop] on-demand full report ${sent ? 'SENT' : 'suppressed by agent'}`);
|
|
560
|
-
})
|
|
561
|
-
.catch((error) => {
|
|
562
|
-
this.deps.log(`[trigger-loop] on-demand full report FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
563
|
-
})
|
|
564
|
-
.finally(() => {
|
|
565
|
-
this.running = false;
|
|
566
|
-
});
|
|
704
|
+
}), 'on-demand full report FAILED');
|
|
567
705
|
return { accepted: true };
|
|
568
706
|
}
|
|
569
707
|
nudge() {
|
|
708
|
+
if (this.stopping)
|
|
709
|
+
return;
|
|
570
710
|
if (this.nudgeTimer)
|
|
571
711
|
return; // already armed - debounce collapses the burst
|
|
572
712
|
const configured = this.deps.config.nudgeDebounceMs;
|
|
@@ -579,14 +719,7 @@ class OperatorTriggerLoop {
|
|
|
579
719
|
this.deps.log('[trigger-loop] nudge: tick already running - skipped (deltas wait for next tick)');
|
|
580
720
|
return;
|
|
581
721
|
}
|
|
582
|
-
this.
|
|
583
|
-
void this.tick()
|
|
584
|
-
.catch((error) => {
|
|
585
|
-
this.deps.log(`[trigger-loop] nudge tick failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
586
|
-
})
|
|
587
|
-
.finally(() => {
|
|
588
|
-
this.running = false;
|
|
589
|
-
});
|
|
722
|
+
this.launchRun(this.tick({ advanceMaintenance: false }), 'nudge tick failed');
|
|
590
723
|
}, debounceMs);
|
|
591
724
|
this.nudgeTimer.unref?.();
|
|
592
725
|
}
|
|
@@ -597,15 +730,10 @@ class OperatorTriggerLoop {
|
|
|
597
730
|
*/
|
|
598
731
|
start() {
|
|
599
732
|
const { config, log } = this.deps;
|
|
733
|
+
this.stopping = false;
|
|
734
|
+
this.schedulerActive = true;
|
|
600
735
|
if ((this.pendingDelivery || this.pendingRequest) && !this.running) {
|
|
601
|
-
this.
|
|
602
|
-
void this.recoverPendingReportWork()
|
|
603
|
-
.catch((error) => {
|
|
604
|
-
log(`[trigger-loop] startup report recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
605
|
-
})
|
|
606
|
-
.finally(() => {
|
|
607
|
-
this.running = false;
|
|
608
|
-
});
|
|
736
|
+
this.launchRun(this.recoverPendingReportWork(), 'startup report recovery failed');
|
|
609
737
|
}
|
|
610
738
|
const handle = setInterval(() => {
|
|
611
739
|
// The INTERVAL is the leg: it beats even while a long tick (full
|
|
@@ -613,29 +741,94 @@ class OperatorTriggerLoop {
|
|
|
613
741
|
// on live. A loop mid-tick is alive, not silent.
|
|
614
742
|
(0, leg_cadence_js_1.getLegCadence)()?.beat('trigger-loop');
|
|
615
743
|
if (this.running) {
|
|
744
|
+
this.maintenancePending = true;
|
|
616
745
|
log('[trigger-loop] tick skipped: previous tick still running');
|
|
617
746
|
return;
|
|
618
747
|
}
|
|
619
|
-
this.
|
|
620
|
-
void this.tick()
|
|
621
|
-
.catch((error) => {
|
|
622
|
-
log(`[trigger-loop] tick failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
623
|
-
})
|
|
624
|
-
.finally(() => {
|
|
625
|
-
this.running = false;
|
|
626
|
-
});
|
|
748
|
+
this.launchRun(this.tick(), 'tick failed');
|
|
627
749
|
}, config.tickMs);
|
|
628
750
|
handle.unref?.();
|
|
629
751
|
log(`[trigger-loop] started (tick every ${config.tickMs}ms)`);
|
|
752
|
+
let stopPromise;
|
|
630
753
|
return () => {
|
|
754
|
+
if (stopPromise)
|
|
755
|
+
return stopPromise;
|
|
756
|
+
this.stopping = true;
|
|
757
|
+
this.schedulerActive = false;
|
|
758
|
+
this.maintenancePending = false;
|
|
631
759
|
clearInterval(handle);
|
|
632
760
|
if (this.nudgeTimer) {
|
|
633
761
|
clearTimeout(this.nudgeTimer);
|
|
634
762
|
this.nudgeTimer = null;
|
|
635
763
|
}
|
|
636
|
-
|
|
764
|
+
stopPromise = (async () => {
|
|
765
|
+
const activeRun = this.activeRunPromise;
|
|
766
|
+
if (activeRun)
|
|
767
|
+
await activeRun;
|
|
768
|
+
log('[trigger-loop] stopped');
|
|
769
|
+
})();
|
|
770
|
+
return stopPromise;
|
|
637
771
|
};
|
|
638
772
|
}
|
|
773
|
+
launchRun(work, failureLabel) {
|
|
774
|
+
this.running = true;
|
|
775
|
+
const tracked = work
|
|
776
|
+
.then(() => undefined)
|
|
777
|
+
.catch((error) => {
|
|
778
|
+
this.deps.log(`[trigger-loop] ${failureLabel}: ${error instanceof Error ? error.message : String(error)}`);
|
|
779
|
+
})
|
|
780
|
+
.finally(() => {
|
|
781
|
+
if (this.activeRunPromise === tracked)
|
|
782
|
+
this.activeRunPromise = null;
|
|
783
|
+
this.finishRun();
|
|
784
|
+
});
|
|
785
|
+
this.activeRunPromise = tracked;
|
|
786
|
+
}
|
|
787
|
+
finishRun() {
|
|
788
|
+
this.running = false;
|
|
789
|
+
if (!this.schedulerActive || !this.maintenancePending)
|
|
790
|
+
return;
|
|
791
|
+
this.maintenancePending = false;
|
|
792
|
+
this.launchRun(this.tick(), 'deferred maintenance tick failed');
|
|
793
|
+
}
|
|
639
794
|
}
|
|
640
795
|
exports.OperatorTriggerLoop = OperatorTriggerLoop;
|
|
796
|
+
const REVIEW_CONTEXT_EVENT_CHARS = 500;
|
|
797
|
+
const REVIEW_CONTEXT_TOTAL_CHARS = 8_000;
|
|
798
|
+
function boundedReviewContext(events) {
|
|
799
|
+
const lines = [];
|
|
800
|
+
let used = 0;
|
|
801
|
+
for (const event of [...events].reverse()) {
|
|
802
|
+
const content = event.content
|
|
803
|
+
.trim()
|
|
804
|
+
.replace(/[\r\n]+/g, ' ')
|
|
805
|
+
.slice(0, REVIEW_CONTEXT_EVENT_CHARS);
|
|
806
|
+
const line = `[${event.channelId}] ${content}`;
|
|
807
|
+
if (used + line.length + 1 > REVIEW_CONTEXT_TOTAL_CHARS)
|
|
808
|
+
break;
|
|
809
|
+
lines.push(line);
|
|
810
|
+
used += line.length + 1;
|
|
811
|
+
}
|
|
812
|
+
const omitted = events.length - lines.length;
|
|
813
|
+
lines.reverse();
|
|
814
|
+
if (omitted > 0)
|
|
815
|
+
lines.unshift(`[... ${omitted} event(s) omitted by review input budget]`);
|
|
816
|
+
return lines;
|
|
817
|
+
}
|
|
818
|
+
function authorWindowFingerprint(events) {
|
|
819
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
820
|
+
for (const event of events) {
|
|
821
|
+
hash.update(event.channel);
|
|
822
|
+
hash.update('\0');
|
|
823
|
+
hash.update(event.channelId);
|
|
824
|
+
hash.update('\0');
|
|
825
|
+
hash.update(String(event.eventIndexId ?? event.id));
|
|
826
|
+
hash.update('\0');
|
|
827
|
+
hash.update(String(event.createdAt));
|
|
828
|
+
hash.update('\0');
|
|
829
|
+
hash.update(event.content);
|
|
830
|
+
hash.update('\0');
|
|
831
|
+
}
|
|
832
|
+
return hash.digest('hex');
|
|
833
|
+
}
|
|
641
834
|
//# sourceMappingURL=operator-trigger-loop.js.map
|