@cohortapp/agent-sdk 2.15.0 → 2.17.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 (38) hide show
  1. package/.env.example +5 -2
  2. package/docs/guides/front-door-session.md +16 -5
  3. package/docs/guides/poller-daemon-setup.md +53 -2
  4. package/lib/assurance/plan-note.mjs +251 -0
  5. package/lib/assurance/plan-note.test.mjs +234 -0
  6. package/lib/assurance/room-budget.mjs +497 -0
  7. package/lib/assurance/room-budget.test.mjs +486 -0
  8. package/lib/assurance/tier.mjs +166 -0
  9. package/lib/assurance/tier.test.mjs +174 -0
  10. package/lib/comms/receipts.mjs +17 -1
  11. package/lib/context/budget.mjs +327 -0
  12. package/lib/context/budget.test.mjs +252 -0
  13. package/lib/context/history-scope.mjs +138 -0
  14. package/lib/context/history-scope.test.mjs +79 -0
  15. package/lib/model-router/economics.mjs +9 -0
  16. package/lib/model-router/resolve.mjs +6 -0
  17. package/lib/org/inbound/facts.mjs +4 -2
  18. package/lib/org/inbound/hydrate.mjs +555 -51
  19. package/lib/org/inbound/hydrate.test.mjs +456 -1
  20. package/package.json +3 -1
  21. package/plugins/maestro-skills/skills/inbound-triage.md +52 -24
  22. package/plugins/maestro-skills/skills/main-session.md +6 -4
  23. package/scripts/daemon/agent-daemon.mjs +35 -7
  24. package/scripts/daemon/agent-daemon.test.mjs +23 -6
  25. package/scripts/daemon/assurance-e2e.test.mjs +75 -19
  26. package/scripts/daemon/assurance.mjs +663 -159
  27. package/scripts/daemon/assurance.test.mjs +820 -140
  28. package/scripts/daemon/context-compiler.mjs +52 -21
  29. package/scripts/daemon/context-compiler.test.mjs +106 -0
  30. package/scripts/daemon/deliver.mjs +7 -4
  31. package/scripts/daemon/dispatcher-session-continuity.test.mjs +365 -0
  32. package/scripts/daemon/dispatcher.mjs +210 -9
  33. package/scripts/daemon/lib/session-router.mjs +310 -42
  34. package/scripts/daemon/lib/session-router.test.mjs +260 -1
  35. package/scripts/daemon/prompt-builder.mjs +160 -16
  36. package/scripts/daemon/prompt-builder.test.mjs +287 -7
  37. package/scripts/daemon/responder-history.test.mjs +37 -1
  38. package/scripts/daemon/responder.mjs +79 -72
@@ -31,7 +31,6 @@ const {
31
31
  shouldAcknowledge,
32
32
  generateAck,
33
33
  sanitiseAckText,
34
- composeProgress,
35
34
  composeFailure,
36
35
  composeSilentSuccess,
37
36
  isOwnNarratorText,
@@ -43,9 +42,11 @@ const {
43
42
  readObligation,
44
43
  openObligations,
45
44
  _resetObligations,
46
- ACK_GRACE_MS,
47
- PROGRESS_AFTER_MS,
48
- PROGRESS_MAX,
45
+ ACK_AFTER_MS,
46
+ ACK_MIN_CHARS,
47
+ RETIRED_NARRATOR_TEMPLATES,
48
+ notePlan,
49
+ NOTICE,
49
50
  STALE_AFTER_MS,
50
51
  INTERRUPT_GRACE_MS,
51
52
  ACK_MAX_ATTEMPTS,
@@ -244,10 +245,14 @@ describe("shouldAcknowledge — the dispatch gate reads item.content", () => {
244
245
  });
245
246
 
246
247
  test("REGRESSION: the daemon's OWN narrator lines are refused — built from the real compose* strings, so the list cannot drift", () => {
247
- const rec = { openedAt: Date.now() - 6 * 60_000, progressSent: 0 };
248
248
  const narrations = [
249
- composeProgress(rec, {}), // "Still on this — N minutes in. …"
250
- composeProgress({ ...rec, progressSent: 1 }, {}), // "Still going — N minutes in. …"
249
+ // The two RETIRED progress lines. Nothing composes them any more, but an
250
+ // older seat on the same fleet still emits them until its autoupdate
251
+ // lands, and acking a peer's progress ping spawns a 45-minute session
252
+ // behind it — the 25/27/30 Aug loop.
253
+ ...RETIRED_NARRATOR_TEMPLATES,
254
+ "Still on this — 7 minutes in. I'll come back as soon as I've got something.",
255
+ "Still going — 22 minutes in. I'll follow up the moment it's done.",
251
256
  composeFailure({}, { failure: { human: "the working session ended unexpectedly" }, willRetry: true }),
252
257
  composeFailure({}, { failure: { human: "the work never came back with a result and has now outrun its time limit" }, willRetry: false }),
253
258
  assurance.composeInterrupted(),
@@ -424,23 +429,44 @@ describe("classifyFailure", () => {
424
429
  // ---------------------------------------------------------------------------
425
430
 
426
431
  describe("openAndAcknowledge", () => {
427
- test("acknowledges and records the debt as acknowledged", async () => {
432
+ test("POLICY: opening a debt says NOTHING — the interim belongs to the sweep", async () => {
433
+ // 3,069 opening acknowledgements in fourteen days were exactly this call
434
+ // speaking at t=0. The record is still written first (a crash between here
435
+ // and the session's first token must leave a debt the sweep finds); what is
436
+ // gone is the reflex in front of it.
428
437
  const t = fakeTransport();
429
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, service: "cohort", deps: { ackSender: t.ackSender } });
430
- assert.equal(r.acked, true);
431
- assert.equal(t.sent.length, 1);
432
- assert.equal(t.sent[0].kind, "ack");
438
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, service: "cohort", tier: "work", deps: { ackSender: t.ackSender } });
439
+ assert.equal(t.sent.length, 0, "not one byte at open time");
440
+ assert.equal(r.acked, false);
441
+ assert.equal(r.ackText, null);
442
+ assert.equal(r.reason, "tier-work-deferred");
433
443
  const rec = readObligation(r.key);
434
- assert.equal(rec.acknowledged, true);
435
- assert.equal(rec.state, "open", "the debt is acknowledged, not discharged — an ack is not an answer");
444
+ assert.equal(rec.state, "open", "the debt is open and owed");
445
+ assert.equal(rec.tier, "work");
446
+ assert.equal(rec.interimSaid, false, "and the one-interim latch is still unspent");
436
447
  });
437
448
 
438
- test("REGRESSION: a FAILED acknowledgement leaves an open debt instead of silence", async () => {
439
- // This is the exact production case: 20 of 32 acknowledgements died and the
440
- // old code did `logResponse(...); return {sent:false}` — the human was never
441
- // told, and nothing ever retried.
449
+ test("the answer tier opens the debt and forbids any interim, ever", async () => {
450
+ const t = fakeTransport();
451
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "answer", ack: false, deps: { ackSender: t.ackSender } });
452
+ assert.equal(t.sent.length, 0);
453
+ const rec = readObligation(r.key);
454
+ assert.equal(rec.interimForbidden, true, "durable on the record — the sweep runs in another tick, maybe another process");
455
+ const after = await sweepObligations({
456
+ now: Date.now() + ACK_AFTER_MS + 60_000,
457
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
458
+ });
459
+ assert.equal(after.acked, 0);
460
+ assert.equal(t.sent.length, 0, "the answer tier is silence, at open and at every sweep after it");
461
+ });
462
+
463
+ test("REGRESSION: an un-acknowledged debt is still visible to the sweep", async () => {
464
+ // The original production case: 20 of 32 acknowledgements died and the old
465
+ // code did `logResponse(...); return {sent:false}` — the human was never
466
+ // told, and nothing ever retried. Nothing is sent at open time any more, so
467
+ // what this now pins is the half that still matters: the debt exists.
442
468
  const t = fakeTransport({ failTimes: 99, error: "claude CLI timed out after 60000ms" });
443
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
469
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work", deps: { ackSender: t.ackSender } });
444
470
  assert.equal(r.acked, false);
445
471
  const rec = readObligation(r.key);
446
472
  assert.equal(rec.acknowledged, false);
@@ -448,32 +474,20 @@ describe("openAndAcknowledge", () => {
448
474
  assert.equal(openObligations().length, 1, "the debt must be visible to the sweep");
449
475
  });
450
476
 
451
- test("REGRESSION: generation failure at open ⇒ NOTHING is sent, and the debt records why", async () => {
452
- // sendHoldingMessage's new contract on a failed generation: {sent:false,
453
- // holdingText:null, genFailed:true}. Silence — never a placeholder.
454
- const t = fakeTransport({ genFail: true });
455
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
456
- assert.equal(r.acked, false);
457
- assert.equal(r.ackText, null, "no text was composed — there is nothing canned to fall back to");
477
+ test("REGRESSION: a generation failure in the interim branch sends NOTHING and records why", async () => {
478
+ // The ack generator's contract on failure is silence, never a placeholder.
479
+ // The branch moved from open time to the sweep; the contract did not.
480
+ const t = fakeTransport();
481
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
482
+ await sweepObligations({
483
+ now: Date.now() + ACK_AFTER_MS + 1000,
484
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => null },
485
+ });
458
486
  assert.equal(t.sent.length, 0, "not one byte reached the human");
459
487
  const rec = readObligation(r.key);
460
488
  assert.equal(rec.acknowledged, false);
461
- assert.equal(rec.ackGenFailures, 1, "the failure is counted, so the sweep's retry is bounded");
462
- assert.equal(rec.state, "open", "the debt survives — progress/failure notices still owe the human");
463
- });
464
-
465
- test("the DEFAULT ack path generates before sending — generation failure short-circuits transport entirely", async () => {
466
- // No injected ackSender here: this drives openAndAcknowledge's own default,
467
- // which must consult generateAck FIRST and never touch transport on null.
468
- const r = await openAndAcknowledge({
469
- item: ITEM,
470
- classResult: CLASS_COMPLEX,
471
- deps: { ackGen: { runImpl: async () => { throw new Error("model down"); } } },
472
- });
473
- assert.equal(r.acked, false);
474
- assert.equal(r.reason, "ack-generation-failed");
475
- assert.equal(r.ackText, null);
476
- assert.equal(readObligation(r.key).state, "open");
489
+ assert.equal(rec.ackGenFailures, 1, "the failure is counted, so the retry is bounded");
490
+ assert.equal(rec.state, "open", "the debt survives — the failure and stale notices still owe the human");
477
491
  });
478
492
 
479
493
  test("the stored summary keeps the classifier's casing — the board row and escalations read from it", async () => {
@@ -488,14 +502,42 @@ describe("openAndAcknowledge", () => {
488
502
  });
489
503
  const rec = readObligation(r.key);
490
504
  assert.equal(rec.summary, "CEO checking in on work progress");
491
- assert.doesNotMatch(composeProgress(rec, { now: Date.now() + 6 * 60_000 }), /CEO checking in on work progress/, "the summary is never leaked into a progress update");
505
+ // It must not leak into anything a human reads. Every deterministic notice
506
+ // the module can still compose is checked, so a future composer that starts
507
+ // interpolating `rec.summary` fails here.
508
+ for (const text of [
509
+ composeFailure(rec, { failure: { human: "the work ran past its time limit and got cut off" }, willRetry: true }),
510
+ composeFailure(rec, { failure: { human: "the work never came back with a result" }, willRetry: false }),
511
+ assurance.composeInterrupted(rec),
512
+ composeSilentSuccess(rec, {}),
513
+ ]) {
514
+ assert.doesNotMatch(text, /CEO checking in on work progress/, "the summary is internal framing, never copy");
515
+ }
492
516
  });
493
517
 
494
- test("acknowledges ONCE — a re-delivered item does not re-announce itself", async () => {
518
+ test("speaks ONCE — a re-delivered item, and a RETRY that reopens the key, do not re-announce", async () => {
519
+ // The retry half is R4: a failed obligation is CLOSED, the item is
520
+ // re-delivered by the next poll, and a brand-new record opens under the
521
+ // same key. Without inheritance that record starts clean and acks from
522
+ // scratch — which is `On it.` arriving immediately after a failure notice,
523
+ // as sampled live.
495
524
  const t = fakeTransport();
496
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
497
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
498
- assert.equal(t.sent.length, 1, "do not spam");
525
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
526
+ await sweepObligations({
527
+ now: Date.now() + ACK_AFTER_MS + 1000,
528
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
529
+ });
530
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1);
531
+ assert.equal(readObligation(r.key).interimSaid, true);
532
+
533
+ assurance.closeObligation(r.key, { outcome: "failed" });
534
+ const again = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
535
+ assert.equal(readObligation(again.key).interimSaid, true, "the latch is INHERITED across the reopen");
536
+ await sweepObligations({
537
+ now: Date.now() + ACK_AFTER_MS + 10 * 60_000,
538
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
539
+ });
540
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "one interim per ask, across every attempt at it");
499
541
  });
500
542
 
501
543
  test("opens NO board row of its own — there is exactly one writer per ask", async () => {
@@ -628,72 +670,134 @@ describe("settleSession", () => {
628
670
  // ---------------------------------------------------------------------------
629
671
 
630
672
  describe("sweepObligations", () => {
631
- test("retries an acknowledgement whose send failed — with a freshly GENERATED line, not a canned one", async () => {
632
- const failing = fakeTransport({ failTimes: 99 });
633
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: failing.ackSender } });
634
- assert.equal(r.acked, false);
673
+ test("THE ONE INTERIM: nothing before ACK_AFTER_MS, then exactly one bespoke line", async () => {
674
+ const t = fakeTransport();
675
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
676
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => "Back on the git fixes now." };
635
677
 
636
- const working = fakeTransport();
637
- const now = Date.now() + ACK_GRACE_MS + 1000;
638
- const stats = await sweepObligations({ now, deps: {
639
- deliverImpl: working.impl,
640
- spokeSinceImpl: () => false,
641
- generateAckImpl: async () => "Back on the git fixes now.",
642
- } });
678
+ const early = await sweepObligations({ now: Date.now() + ACK_AFTER_MS - 1000, deps });
679
+ assert.equal(early.acked, 0);
680
+ assert.equal(t.sent.length, 0, "below ninety seconds the typing indicator IS the acknowledgement");
681
+
682
+ const stats = await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps });
643
683
  assert.equal(stats.acked, 1);
644
- assert.equal(working.sent[0].text, "Back on the git fixes now.");
645
- assert.doesNotMatch(working.sent[0].text, /^(on it|looking now)/i, "never the retired rotation");
646
- assert.equal(readObligation(r.key).acknowledged, true);
684
+ assert.equal(t.sent[0].kind, "ack");
685
+ assert.equal(t.sent[0].text, "Back on the git fixes now.");
686
+ assert.doesNotMatch(t.sent[0].text, /^(on it|looking now)/i, "never the retired rotation");
687
+ assert.equal(readObligation(r.key).interimSaid, true);
688
+
689
+ // …and never a second one, however long the work runs.
690
+ for (let m = 2; m < 40; m++) {
691
+ const now = Date.now() + ACK_AFTER_MS + m * 60_000;
692
+ if (now - Date.now() >= STALE_AFTER_MS) break;
693
+ await sweepObligations({ now, deps });
694
+ }
695
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "one interim per obligation, full stop");
696
+ });
697
+
698
+ test("ONE SEAT, eight concurrent obligations in ONE room, exactly ONE interim", async () => {
699
+ // WHAT THIS IS AND IS NOT. It is the per-seat guarantee: one daemon holding
700
+ // eight concurrent debts in one channel emits one holding line, not eight.
701
+ //
702
+ // It is NOT `capital-formation` reproduced, and the title used to say it
703
+ // was. That room's 9,187 messages in 21 days came from EIGHT SEPARATE
704
+ // SEATS, each with its own AGENT_DIR on its own machine and therefore its
705
+ // own ledger — see room-budget.test.mjs, "the budget is per (SEAT, service,
706
+ // channel)". This gate's ceiling in that room is 8 x (60/15) = 32/h against
707
+ // a measured content-free rate of ~14/h, so it would not have been the
708
+ // binding constraint there. What removed the measured traffic is the
709
+ // deleted progress branch (961 -> 0), the sanitiser, and TIME+ONCE; this
710
+ // gate is the backstop that bounds what survives all three.
711
+ const t = fakeTransport();
712
+ for (let i = 0; i < 8; i++) {
713
+ await openAndAcknowledge({
714
+ item: { ...ITEM, id: `msg-${i}`, raw_ref: `cohort:capform:${i}`, message_id: `${i}` },
715
+ classResult: CLASS_COMPLEX,
716
+ tier: "work",
717
+ });
718
+ }
719
+ assert.equal(openObligations().length, 8, "every ask is still opened and tracked");
720
+
721
+ const stats = await sweepObligations({
722
+ now: Date.now() + ACK_AFTER_MS + 1000,
723
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
724
+ });
725
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, `eight obligations, one message — got ${t.sent.length}`);
726
+ assert.equal(stats.acked, 1);
727
+ assert.equal(stats.suppressed, 7, "…and the seven suppressions are COUNTED, so a quiet sweep can still prove it decided");
728
+
729
+ // Over budget is not a drop: every debt is still open and still owed.
730
+ assert.equal(openObligations().length, 8);
647
731
  });
648
732
 
649
- test("REGRESSION: a failed generation in the sweep sends NOTHING — bounded, then given up on", async () => {
650
- const t = fakeTransport({ genFail: true }); // the open-time generation already failed once
651
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
652
- assert.equal(r.acked, false);
733
+ test("…and the suppressed debts stay silent afterwards rather than queueing up a burst", async () => {
734
+ const t = fakeTransport();
735
+ for (let i = 0; i < 8; i++) {
736
+ await openAndAcknowledge({ item: { ...ITEM, id: `m-${i}`, raw_ref: `cohort:capform2:${i}` }, classResult: CLASS_COMPLEX, tier: "work" });
737
+ }
738
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
739
+ await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps });
740
+ await sweepObligations({ now: Date.now() + 20 * 60_000, deps });
741
+ await sweepObligations({ now: Date.now() + 40 * 60_000, deps });
742
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "the window reopening must not release a backlog");
743
+ });
744
+
745
+ test("a DIFFERENT room is never gagged by a busy one", async () => {
746
+ const t = fakeTransport();
747
+ for (let i = 0; i < 5; i++) {
748
+ await openAndAcknowledge({ item: { ...ITEM, id: `b-${i}`, raw_ref: `cohort:busy:${i}` }, classResult: CLASS_COMPLEX, tier: "work" });
749
+ }
750
+ await openAndAcknowledge({
751
+ item: { ...ITEM, id: "quiet-1", raw_ref: "cohort:quiet:1", channel_id: "C-QUIET" },
752
+ classResult: CLASS_COMPLEX,
753
+ tier: "work",
754
+ });
755
+ const stats = await sweepObligations({
756
+ now: Date.now() + ACK_AFTER_MS + 1000,
757
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
758
+ });
759
+ assert.equal(stats.acked, 2, "one per room");
760
+ assert.equal(new Set(t.sent.map((m) => m.channel)).size, 2);
761
+ });
762
+
763
+ test("REGRESSION: a failed generation sends NOTHING — bounded, then given up on", async () => {
764
+ const t = fakeTransport();
765
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
653
766
 
654
767
  let genCalls = 0;
655
768
  const t0 = Date.now();
656
769
  for (let m = 1; m <= 8; m++) {
657
- await sweepObligations({ now: t0 + ACK_GRACE_MS + m * 60_000, deps: {
770
+ await sweepObligations({ now: t0 + ACK_AFTER_MS + m * 60_000, deps: {
658
771
  deliverImpl: t.impl,
659
772
  spokeSinceImpl: () => false,
660
773
  generateAckImpl: async () => { genCalls++; return null; },
661
774
  } });
662
775
  }
663
776
  assert.equal(t.sent.filter((s) => s.kind === "ack").length, 0, "no canned line went out in place of the failed generation");
664
- assert.ok(genCalls < ACK_GEN_MAX_FAILURES + 1, `generation retries are bounded, not once-a-tick forever (saw ${genCalls})`);
777
+ assert.ok(genCalls <= ACK_GEN_MAX_FAILURES, `generation retries are bounded, not once-a-tick forever (saw ${genCalls})`);
665
778
  const rec = readObligation(r.key);
666
- assert.equal(rec.ackSuppressed, true, "the ack is given up on…");
667
- assert.equal(rec.state, "open", "…but the DEBT is not — progress and failure notices still speak");
668
- assert.ok(t.sent.some((s) => s.kind === "progress"), "and indeed the human still hears about the long-running work");
669
- });
670
-
671
- test("long work gets an interim update, not silence", async () => {
672
- const t = fakeTransport();
673
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
674
- const now = Date.now() + PROGRESS_AFTER_MS + 1000;
675
- const stats = await sweepObligations({ now, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
676
- assert.equal(stats.progressed, 1);
677
- const msg = t.sent.at(-1).text;
678
- assert.match(msg, /still on this/i);
679
- assert.match(msg, /minutes in/i, "an interim update that omits how long it has been is not much of an update");
680
- // Six minutes in is INSIDE the 10-20 minute window the ack just promised
681
- // for opus work; claiming it here contradicts a message the same human read.
682
- assert.doesNotMatch(msg, /longer than I expected/i);
779
+ assert.equal(rec.ackSuppressed, true, "the interim is given up on…");
780
+ assert.equal(rec.state, "open", "…but the DEBT is not — the stale check still owes the human");
683
781
  });
684
782
 
685
- test("interim updates are CAPPED — attentive, not noisy", async () => {
783
+ test("a failed generation does NOT spend the room's budget", async () => {
784
+ // Ask, compose, THEN spend. A budget burned on a message that was never
785
+ // composed gags the room for fifteen minutes in exchange for nothing.
686
786
  const t = fakeTransport();
687
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
688
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false };
689
- let now = Date.now();
690
- for (let i = 0; i < 10; i++) {
691
- now += PROGRESS_AFTER_MS + 60_000 * 11;
692
- if (now - Date.now() >= STALE_AFTER_MS) break;
693
- await sweepObligations({ now, deps });
694
- }
695
- const progressCount = t.sent.filter((m) => m.kind === "progress").length;
696
- assert.ok(progressCount <= PROGRESS_MAX, `sent ${progressCount} progress updates; cap is ${PROGRESS_MAX}`);
787
+ await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
788
+ await openAndAcknowledge({ item: { ...ITEM, id: "x2", raw_ref: "cohort:same-room:2" }, classResult: CLASS_COMPLEX, tier: "work" });
789
+ let calls = 0;
790
+ const stats = await sweepObligations({
791
+ now: Date.now() + ACK_AFTER_MS + 1000,
792
+ deps: {
793
+ deliverImpl: t.impl,
794
+ spokeSinceImpl: () => false,
795
+ // The first obligation's generation fails; the second's succeeds.
796
+ generateAckImpl: async () => (++calls === 1 ? null : "Back on the git fixes now."),
797
+ },
798
+ });
799
+ assert.equal(stats.acked, 1, "the room's one interim went to the ask that could actually compose one");
800
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1);
697
801
  });
698
802
 
699
803
  test("work that outruns every session timeout is declared dead, not left hanging", async () => {
@@ -728,10 +832,33 @@ describe("sweepObligations", () => {
728
832
 
729
833
  const stats = await sweepObligations({
730
834
  now: Date.now() + INTERRUPT_GRACE_MS + 1000,
731
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false },
835
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
732
836
  });
733
837
  assert.equal(stats.interrupted, 1);
734
838
  assert.match(t.sent.at(-1).text, /was interrupted before it finished/i);
839
+ assert.equal(t.sent.at(-1).kind, "notice", "an interrupted notice is an OUTCOME, not a progress ping — the kind it used to borrow no longer exists");
840
+ });
841
+
842
+ test("…and a SECOND interruption of the same ask says nothing more", async () => {
843
+ // Said once per (channel, obligationKey). Two daemon restarts over one long
844
+ // ask are one apology, not two — and a retry that reopens the same key
845
+ // inherits the notice ledger, so it is not two either.
846
+ const t = fakeTransport();
847
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
848
+ const { writeFileSync } = await import("node:fs");
849
+ const orphan = (extra = {}) => {
850
+ const rec = { ...readObligation(r.key), daemonPid: process.pid + 99999, ...extra };
851
+ writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
852
+ };
853
+ orphan();
854
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
855
+ await sweepObligations({ now: Date.now() + INTERRUPT_GRACE_MS + 1000, deps });
856
+ assert.equal(t.sent.filter((m) => /interrupted/i.test(m.text)).length, 1);
857
+
858
+ orphan({ interruptedNotifiedAt: null });
859
+ const second = await sweepObligations({ now: Date.now() + INTERRUPT_GRACE_MS + 120_000, deps });
860
+ assert.equal(second.interrupted, 0);
861
+ assert.equal(t.sent.filter((m) => /interrupted/i.test(m.text)).length, 1, "one apology per ask, per room");
735
862
  });
736
863
 
737
864
  test("REGRESSION: a LIVE daemon's obligation is never declared interrupted", async () => {
@@ -750,7 +877,7 @@ describe("sweepObligations", () => {
750
877
  const before = t.sent.length;
751
878
  const stats = await sweepObligations({
752
879
  now: Date.now() + INTERRUPT_GRACE_MS + 1000,
753
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => true },
880
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => true, generateAckImpl: async () => FAKE_ACK_LINE },
754
881
  });
755
882
  assert.equal(stats.interrupted, 0, "the owning daemon is alive — its work is not interrupted");
756
883
  assert.equal(t.sent.length, before, "and nobody is told a lie about it");
@@ -768,7 +895,7 @@ describe("sweepObligations", () => {
768
895
 
769
896
  const stats = await sweepObligations({
770
897
  now: Date.now() + 1000,
771
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false },
898
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
772
899
  });
773
900
  assert.equal(stats.interrupted, 0);
774
901
  });
@@ -779,7 +906,7 @@ describe("sweepObligations", () => {
779
906
  // the owner is alive and will; two daemons acking is one human hearing
780
907
  // double.
781
908
  const failing = fakeTransport({ failTimes: 99 });
782
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: failing.ackSender } });
909
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work", deps: { ackSender: failing.ackSender } });
783
910
  assert.equal(r.acked, false);
784
911
  const rec = readObligation(r.key);
785
912
  rec.daemonPid = process.pid + 99999;
@@ -788,7 +915,7 @@ describe("sweepObligations", () => {
788
915
 
789
916
  const working = fakeTransport();
790
917
  const stats = await sweepObligations({
791
- now: Date.now() + ACK_GRACE_MS + 1000,
918
+ now: Date.now() + ACK_AFTER_MS + 1000,
792
919
  deps: {
793
920
  deliverImpl: working.impl,
794
921
  spokeSinceImpl: () => false,
@@ -799,11 +926,12 @@ describe("sweepObligations", () => {
799
926
  assert.equal(stats.acked, 0, "the live owner narrates its own debt");
800
927
  assert.equal(working.sent.length, 0, "not one byte from the non-owner");
801
928
  assert.equal(readObligation(r.key).acknowledged, false);
929
+ assert.equal(readObligation(r.key).interimSaid, false, "and the non-owner does not spend the latch either");
802
930
  });
803
931
 
804
- test("…but once the owning daemon is DEAD, this daemon takes the ack over", async () => {
932
+ test("…but once the owning daemon is DEAD, this daemon takes the interim over", async () => {
805
933
  const failing = fakeTransport({ failTimes: 99 });
806
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: failing.ackSender } });
934
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work", deps: { ackSender: failing.ackSender } });
807
935
  const rec = readObligation(r.key);
808
936
  rec.daemonPid = process.pid + 99999;
809
937
  const { writeFileSync } = await import("node:fs");
@@ -811,7 +939,7 @@ describe("sweepObligations", () => {
811
939
 
812
940
  const working = fakeTransport();
813
941
  const stats = await sweepObligations({
814
- now: Date.now() + ACK_GRACE_MS + 1000,
942
+ now: Date.now() + ACK_AFTER_MS + 1000,
815
943
  deps: {
816
944
  deliverImpl: working.impl,
817
945
  spokeSinceImpl: () => false,
@@ -823,27 +951,26 @@ describe("sweepObligations", () => {
823
951
  assert.equal(readObligation(r.key).acknowledged, true);
824
952
  });
825
953
 
826
- test("REGRESSION: a debt OWNED by another LIVE daemon gets no progress ping from this one", async () => {
954
+ test("REGRESSION: a debt OWNED by another LIVE daemon gets no interim from this one", async () => {
827
955
  const t = fakeTransport();
828
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
956
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
829
957
  const rec = readObligation(r.key);
830
958
  rec.daemonPid = process.pid + 99999;
831
959
  const { writeFileSync } = await import("node:fs");
832
960
  writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
833
961
 
834
- const before = t.sent.length;
835
962
  const stats = await sweepObligations({
836
- now: Date.now() + PROGRESS_AFTER_MS + 1000,
837
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => true },
963
+ now: Date.now() + ACK_AFTER_MS + 1000,
964
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => true, generateAckImpl: async () => FAKE_ACK_LINE },
838
965
  });
839
- assert.equal(stats.progressed, 0, "the live owner sends its own progress updates");
966
+ assert.equal(stats.acked, 0, "the live owner speaks for its own debt");
840
967
  assert.equal(stats.interrupted, 0, "and a live owner is certainly not 'interrupted'");
841
- assert.equal(t.sent.length, before);
968
+ assert.equal(t.sent.length, 0);
842
969
  });
843
970
 
844
- test("…and a dead owner's debt DOES get this daemon's progress ping (after the interrupted notice is done)", async () => {
971
+ test("…and a dead owner's debt DOES get this daemon's interim (after the interrupted notice is done)", async () => {
845
972
  const t = fakeTransport();
846
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
973
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
847
974
  const rec = readObligation(r.key);
848
975
  rec.daemonPid = process.pid + 99999;
849
976
  rec.interruptedNotifiedAt = Date.now(); // the orphan apology already went out
@@ -851,41 +978,43 @@ describe("sweepObligations", () => {
851
978
  writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
852
979
 
853
980
  const stats = await sweepObligations({
854
- now: Date.now() + PROGRESS_AFTER_MS + 1000,
855
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false },
981
+ now: Date.now() + ACK_AFTER_MS + 1000,
982
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => "Back on the git fixes now." },
856
983
  });
857
- assert.equal(stats.progressed, 1);
858
- assert.match(t.sent.at(-1).text, /still on this/i);
984
+ assert.equal(stats.acked, 1);
985
+ assert.equal(t.sent.at(-1).text, "Back on the git fixes now.");
859
986
  });
860
987
 
861
988
  test("each distinct message carries a distinct idempotency suffix", async () => {
862
- // hq de-dups server-side on the client message id. Two progress updates that
863
- // both key on "progress" are ONE message as far as hq is concerned, so the
864
- // second would be silently swallowed — "never silent" degrading into
865
- // "acknowledged once, then silent forever". This is invisible in any test
866
- // that stubs transport without inspecting the options, hence this one.
989
+ // hq de-dups server-side on the client message id, so two messages that key
990
+ // on the same suffix are ONE message as far as hq is concerned and the
991
+ // second is silently swallowed — "never silent" degrading into "spoke once,
992
+ // then silent forever". Invisible to any test that stubs transport without
993
+ // inspecting the options, hence this one.
867
994
  const seen = [];
868
995
  const impl = async (item, text, o = {}) => { seen.push(o.idempotencySuffix); return { sent: true, via: "fake", channel: "c" }; };
869
- const t = fakeTransport();
870
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
996
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
871
997
 
872
- const deps = { deliverImpl: impl, spokeSinceImpl: () => false };
873
- await sweepObligations({ now: Date.now() + PROGRESS_AFTER_MS + 1000, deps });
874
- await sweepObligations({ now: Date.now() + PROGRESS_AFTER_MS + 1000 + assurance.PROGRESS_EVERY_MS + 1000, deps });
998
+ const deps = { deliverImpl: impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
999
+ await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps }); // the interim
1000
+ await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps }); // the failure notice
1001
+ await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 1000, deps }); // the stale notice
875
1002
 
876
- assert.ok(seen.length >= 2, `expected two progress sends, saw ${seen.length}`);
1003
+ assert.ok(seen.length >= 2, `expected several distinct sends, saw ${seen.length}`);
877
1004
  assert.equal(new Set(seen).size, seen.length, `suffixes must be unique per message, got ${JSON.stringify(seen)}`);
878
1005
  });
879
1006
 
880
1007
  test("emits at most ONE message per obligation per tick", async () => {
881
- const t = fakeTransport({ failTimes: 1 }); // the ack fails
882
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
883
- assert.equal(r.acked, false);
884
- const before = t.sent.length;
885
- // Old enough to qualify for BOTH the ack retry and a progress update.
886
- const now = Date.now() + PROGRESS_AFTER_MS + 1000;
887
- await sweepObligations({ now, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE } });
888
- assert.equal(t.sent.length - before, 1, "two messages in one tick reads as a malfunction");
1008
+ const t = fakeTransport();
1009
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1010
+ // Old enough to qualify for BOTH the interrupted notice and the interim.
1011
+ const rec = readObligation(r.key);
1012
+ rec.daemonPid = process.pid + 99999;
1013
+ const { writeFileSync } = await import("node:fs");
1014
+ writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
1015
+ const now = Date.now() + INTERRUPT_GRACE_MS + 1000;
1016
+ await sweepObligations({ now, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE } });
1017
+ assert.equal(t.sent.length, 1, "two messages in one tick reads as a malfunction");
889
1018
  });
890
1019
  });
891
1020
 
@@ -910,6 +1039,67 @@ describe("delivery receipts", () => {
910
1039
  );
911
1040
  });
912
1041
 
1042
+ test("REGRESSION: an 'interrupted' notice is not an answer either", async () => {
1043
+ // The interrupted apology moved from kind "progress" to kind "notice" when
1044
+ // progress pings were deleted. A kind that is not in the non-answer set
1045
+ // reads to `spokeFor` as "the human was spoken to", which would discharge
1046
+ // an unanswered debt the moment a daemon restarted — the exact class of bug
1047
+ // the receipt ledger exists to prevent.
1048
+ const { NON_ANSWER_KINDS } = await import("../../lib/comms/receipts.mjs");
1049
+ assert.ok(NON_ANSWER_KINDS.includes("notice"), "every courtesy kind the daemon can emit belongs in the non-answer set");
1050
+ const now = Date.now();
1051
+ receipts.recordOutbound({ service: "cohort", channel: "C-NOTICE", kind: "notice", agentRoot: ROOT, now });
1052
+ assert.equal(
1053
+ receipts.spokeFor({ obligation: { key: "k", channel: "C-NOTICE", openedAt: now - 1000 }, now, siblingOpenCount: 1, agentRoot: ROOT }).heard,
1054
+ false,
1055
+ );
1056
+ });
1057
+
1058
+ test("every kind the daemon actually sends is classified as answer or courtesy — none left unclassified", async () => {
1059
+ // A new kind that nobody adds to the set silently becomes "this discharged
1060
+ // the debt" — the discharge-an-unanswered-debt bug the receipt ledger
1061
+ // exists to prevent.
1062
+ //
1063
+ // DERIVED FROM THE EMITTERS, not hand-listed. The hand-written literal this
1064
+ // replaces could not fail when a kind was added, and had ALREADY missed one:
1065
+ // `plan` was being emitted by notePlan and was absent from the list. Same
1066
+ // shape as narratorPatterns(), which derives its strings from the composers
1067
+ // rather than restating them.
1068
+ const { NON_ANSWER_KINDS } = await import("../../lib/comms/receipts.mjs");
1069
+ const { readFileSync } = await import("node:fs");
1070
+ const here = new URL(".", import.meta.url).pathname;
1071
+ // Line-scoped, and `counters.bump` lines are skipped: a counter LABEL is
1072
+ // not a delivery kind (responder.mjs tags its dedup/generation counters
1073
+ // `kind: "holding"`, which never reaches a receipt). Erring toward a false
1074
+ // alarm is deliberate — a spurious failure sends someone to look, a silent
1075
+ // miss is the bug this test is for.
1076
+ const emitted = new Set();
1077
+ for (const f of ["assurance.mjs", "responder.mjs"]) {
1078
+ for (const line of readFileSync(join(here, f), "utf-8").split("\n")) {
1079
+ if (line.includes("counters.bump")) continue;
1080
+ const m = /\bkind:\s*"([a-z-]+)"/.exec(line);
1081
+ if (m) emitted.add(m[1]);
1082
+ }
1083
+ }
1084
+ assert.ok(emitted.size >= 5, `the scan must find the emitters — found ${[...emitted].join(", ")}`);
1085
+ // The ONE kind that is an answer. Everything else the daemon emits about an
1086
+ // ask is a courtesy and must not discharge the debt.
1087
+ const ANSWERS = new Set(["reply"]);
1088
+ for (const kind of emitted) {
1089
+ assert.equal(
1090
+ NON_ANSWER_KINDS.includes(kind),
1091
+ !ANSWERS.has(kind),
1092
+ `"${kind}" is emitted by the daemon and is ${ANSWERS.has(kind) ? "an answer" : "a courtesy"} — lib/comms/receipts.mjs NON_ANSWER_KINDS must say so`,
1093
+ );
1094
+ }
1095
+ // Pinned explicitly as well, so a scan that silently stops matching is also
1096
+ // a failure rather than a green run over an empty set.
1097
+ for (const kind of ["ack", "notice", "failure", "plan"]) {
1098
+ assert.ok(emitted.has(kind), `"${kind}" must still be found by the scan`);
1099
+ assert.ok(NON_ANSWER_KINDS.includes(kind), `"${kind}" is a courtesy`);
1100
+ }
1101
+ });
1102
+
913
1103
  test("an unreadable ledger reads as 'the human heard nothing', never as 'fine'", () => {
914
1104
  // The fail-open direction is deliberate: the cost of guessing wrong this way
915
1105
  // is one redundant message; guessing the other way is the bug being fixed.
@@ -998,15 +1188,15 @@ describe("attribution: which debt did that message actually discharge", () => {
998
1188
  // A debt whose acknowledgement failed, and something unrelated posts into the
999
1189
  // room. Closing on that leaves a human never acknowledged, never answered,
1000
1190
  // and the debt recorded as discharged.
1001
- const t = fakeTransport({ failTimes: 1 });
1191
+ const t = fakeTransport();
1002
1192
  const solo = { ...ITEM, id: "PRE-1", raw_ref: "cohort:C-PRE:PRE-1", channel: "C-PRE", channel_id: "C-PRE" };
1003
- const r = await openAndAcknowledge({ item: solo, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1004
- assert.equal(readObligation(r.key).acknowledged, false);
1193
+ const r = await openAndAcknowledge({ item: solo, classResult: CLASS_COMPLEX, tier: "work" });
1194
+ assert.equal(readObligation(r.key).acknowledged, false, "nothing is said at open time any more");
1005
1195
  receipts.recordOutbound({ service: "cohort", channel: "C-PRE", kind: "reply", agentRoot: ROOT });
1006
1196
 
1007
- const stats = await sweepObligations({ now: Date.now() + ACK_GRACE_MS + 1000, deps: { deliverImpl: t.impl, generateAckImpl: async () => "Finally picking your ask up now." } });
1197
+ const stats = await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps: { deliverImpl: t.impl, generateAckImpl: async () => "Finally picking your ask up now." } });
1008
1198
  assert.equal(stats.closed, 0, "no session of ours had run — that message was not our answer");
1009
- assert.equal(stats.acked, 1, "instead the missing acknowledgement is finally sent");
1199
+ assert.equal(stats.acked, 1, "instead the interim is finally sent");
1010
1200
  assert.equal(t.sent.at(-1).text, "Finally picking your ask up now.");
1011
1201
  });
1012
1202
  });
@@ -1109,3 +1299,493 @@ describe("a running retry is not declared dead", () => {
1109
1299
  assert.match(t.sent.at(-1).text, /outrun its time limit/i);
1110
1300
  });
1111
1301
  });
1302
+
1303
+ // ---------------------------------------------------------------------------
1304
+ // 10. ACKNOWLEDGEMENT DISCIPLINE — the 2026-09-12 policy, against the measured
1305
+ // production strings that made it necessary.
1306
+ // ---------------------------------------------------------------------------
1307
+
1308
+ describe("sanitiseAckText enforces the prompt the model was already given", () => {
1309
+ // The ack system prompt has said "Never a generic 'on it' or 'looking into
1310
+ // it'" since it was written. Nothing checked. 3,069 generic acknowledgements
1311
+ // shipped in fourteen days — 28.8% of every message this fleet sent — and 272
1312
+ // of them were never followed by a substantive reply within the hour.
1313
+ //
1314
+ // These are the REAL strings, as observed in org_default_adaptic.
1315
+ const OBSERVED_IN_PRODUCTION = [
1316
+ "On it.",
1317
+ "On it",
1318
+ "On it — digging in now.",
1319
+ "Looking now.",
1320
+ "Looking into it.",
1321
+ "Looking into this now.",
1322
+ "Checking.",
1323
+ "Checking now.",
1324
+ "One moment.",
1325
+ "Got it.",
1326
+ "Got it — will do.",
1327
+ "Will do.",
1328
+ "Working on it.",
1329
+ "Taking a look.",
1330
+ "Digging in now.",
1331
+ ];
1332
+
1333
+ for (const line of OBSERVED_IN_PRODUCTION) {
1334
+ test(`rejects the measured generic ack: ${JSON.stringify(line)}`, () => {
1335
+ assert.equal(sanitiseAckText(line), null, "rejection ⇒ null ⇒ send nothing, which is the existing correct failure mode");
1336
+ });
1337
+ }
1338
+
1339
+ test("rejects anything under the minimum length, whatever its phrasing", () => {
1340
+ // The second, phrasing-independent net. A line too short to name the thing
1341
+ // that was asked cannot be specific to it, which is the one thing the
1342
+ // prompt requires.
1343
+ assert.equal(ACK_MIN_CHARS, 20);
1344
+ assert.equal(sanitiseAckText("Sorted, starting."), null);
1345
+ assert.equal(sanitiseAckText("Yep."), null);
1346
+ assert.equal(sanitiseAckText("Onto it now, boss."), null);
1347
+ });
1348
+
1349
+ test("…and still accepts a line that actually names the ask", () => {
1350
+ // The discipline must not become a gag. A bespoke line is exactly what the
1351
+ // prompt asks for, and it survives.
1352
+ for (const good of [
1353
+ "Give me a few minutes on the git fixes.",
1354
+ "Pulling the July ledger against the forecast now.",
1355
+ "Starting on the regulator gap analysis — the Thursday one.",
1356
+ ]) {
1357
+ assert.equal(sanitiseAckText(good), good, `"${good}" is specific and must pass`);
1358
+ }
1359
+ });
1360
+
1361
+ test("REGRESSION: the ack prompt and the filter read the same pattern, so they cannot drift again", async () => {
1362
+ // The whole defect was an instruction with no enforcement behind it. The
1363
+ // filter now imports GENERIC_OPENER from the copy-rule module; this pins
1364
+ // that every opener the prompt names is in fact refused.
1365
+ const { GENERIC_OPENER } = await import("../../lib/assurance/plan-note.mjs");
1366
+ for (const opener of ["on it", "looking now", "looking into", "checking", "one moment", "got it", "will do", "working on it", "taking a look", "digging in"]) {
1367
+ assert.match(opener, GENERIC_OPENER, `the prompt forbids "${opener}" — the filter must too`);
1368
+ }
1369
+ });
1370
+ });
1371
+
1372
+ describe("failure notices are said ONCE per (channel, obligationKey)", () => {
1373
+ test("REGRESSION: the same failure sentence does not reach one room three times in a minute", async () => {
1374
+ // Sampled live at 06:32Z on 2026-09-12: the same failure sentence three
1375
+ // times inside one minute, and `On it.` again immediately after. The cause
1376
+ // was a retry loop that narrated every attempt — notice, retry, a FRESH
1377
+ // obligation under the same key, notice again — with nothing suppressing
1378
+ // the repeat.
1379
+ const t = fakeTransport();
1380
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1381
+
1382
+ for (let attempt = 0; attempt < 3; attempt++) {
1383
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1384
+ assurance.noteSession(r.key, `s-${attempt}`);
1385
+ await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps });
1386
+ // Whatever the outcome, the item is re-delivered and the key reopens.
1387
+ assurance.closeObligation(r.key, { outcome: "failed" });
1388
+ }
1389
+
1390
+ const failures = t.sent.filter((m) => m.kind === "failure");
1391
+ assert.equal(failures.length, 1, `the same sentence went out ${failures.length} times; the whole point is once`);
1392
+ assert.match(failures[0].text, /hit a problem/i);
1393
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 0, "and no `On it.` behind it either");
1394
+ });
1395
+
1396
+ test("…but 'a retry is running' and 'I have stopped' remain two separate, sayable facts", async () => {
1397
+ // Collapsing them would leave a human who was told a retry was in flight
1398
+ // never learning it had been abandoned. What is suppressed is the REPEAT of
1399
+ // one fact, not the second fact.
1400
+ const t = fakeTransport();
1401
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false };
1402
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1403
+ assurance.noteSession(r.key, "s-1");
1404
+ const first = await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps });
1405
+ assert.equal(first.willRetry, true);
1406
+
1407
+ // The retry runs and fails terminally.
1408
+ assurance.noteSession(r.key, "s-2");
1409
+ const second = await settleSession({ key: r.key, ok: false, code: null, error: "claude CLI spawn error: ENOENT", deps });
1410
+ assert.equal(second.willRetry, false);
1411
+
1412
+ const texts = t.sent.filter((m) => m.kind === "failure").map((m) => m.text);
1413
+ assert.equal(texts.length, 2);
1414
+ assert.match(texts[0], /retrying now/i);
1415
+ assert.match(texts[1], /stopped retrying/i);
1416
+ });
1417
+
1418
+ test("the notice ledger records WHICH notice reached WHICH room", async () => {
1419
+ const t = fakeTransport();
1420
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1421
+ await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
1422
+ const rec = readObligation(r.key);
1423
+ const ids = Object.keys(rec.notices || {});
1424
+ assert.equal(ids.length, 1);
1425
+ assert.ok(ids[0].startsWith(NOTICE.FAILURE_RETRYING), `expected a ${NOTICE.FAILURE_RETRYING} stamp, got ${ids[0]}`);
1426
+ assert.ok(ids[0].includes(String(ITEM.channel_id).toLowerCase()), "the channel is part of the key — the same ask re-routed elsewhere genuinely has not been answered there");
1427
+ });
1428
+
1429
+ test("a stale notice is not repeated for a key that already heard one", async () => {
1430
+ const t = fakeTransport();
1431
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1432
+ for (let attempt = 0; attempt < 3; attempt++) {
1433
+ await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1434
+ await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 1000, deps });
1435
+ }
1436
+ assert.equal(t.sent.filter((m) => m.kind === "failure").length, 1);
1437
+ });
1438
+
1439
+ // ── THE SIBLING SHAPE, which the per-record ledger structurally cannot reach ─
1440
+ //
1441
+ // The dedupe above is keyed on the obligation RECORD, so it covers the RETRY
1442
+ // loop: notice → retry → fresh record under the same key → notice again. It
1443
+ // does NOT cover SIBLINGS. Twenty separate inbound asks in one room whose
1444
+ // sessions all die the same way are twenty separate records, and the stale
1445
+ // branch's sentence contains nothing about which ask it is — so the room gets
1446
+ // the identical line twenty times and the reader cannot tell any of them
1447
+ // apart. That is the commoner shape whenever a dispatcher or model path is
1448
+ // down, and it is the one the sampled "same failure sentence three times in
1449
+ // one minute" is most likely to have actually been.
1450
+
1451
+ test("REGRESSION: twenty SIBLING debts staling together say the identical sentence ONCE", async () => {
1452
+ const t = fakeTransport();
1453
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1454
+ for (let i = 0; i < 20; i++) {
1455
+ await openAndAcknowledge({
1456
+ item: { ...ITEM, id: `sib-${i}`, raw_ref: `cohort:sibling:${i}`, message_id: `${i}` },
1457
+ classResult: CLASS_COMPLEX,
1458
+ tier: "work",
1459
+ });
1460
+ }
1461
+ assert.equal(openObligations().length, 20, "every ask is still opened and tracked");
1462
+
1463
+ await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 60_000, deps });
1464
+
1465
+ const failures = t.sent.filter((m) => m.kind === "failure");
1466
+ assert.equal(failures.length, 1, `twenty byte-identical failure sentences reached one room ${failures.length} times`);
1467
+ // Nothing is LOST: every dead ask is still escalated for an operator, and
1468
+ // every debt is still closed. Only the duplicate sentence is withheld.
1469
+ assert.equal(openObligations().length, 0, "all twenty debts were still discharged");
1470
+ const { readdirSync } = await import("node:fs");
1471
+ assert.equal(readdirSync(join(ROOT, "state", "obligations", "needs-attention")).length, 20,
1472
+ "…and an operator still sees all twenty in needs-attention");
1473
+ });
1474
+
1475
+ test("…but twenty siblings that fail DIFFERENTLY are all said, because each is a fact the reader lacks", async () => {
1476
+ // The safety argument for the suppression above. Only an IDENTICAL sentence
1477
+ // is withheld; suppressing information a person does not have re-creates
1478
+ // the original bug — someone never told their work died.
1479
+ const t = fakeTransport();
1480
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false };
1481
+ const CAUSES = [
1482
+ { code: null, error: "claude CLI spawn error: ENOENT" },
1483
+ { code: 0, error: "" },
1484
+ ];
1485
+ let sent = 0;
1486
+ for (let i = 0; i < CAUSES.length; i++) {
1487
+ const r = await openAndAcknowledge({
1488
+ item: { ...ITEM, id: `diff-${i}`, raw_ref: `cohort:different:${i}` },
1489
+ classResult: CLASS_COMPLEX,
1490
+ tier: "work",
1491
+ });
1492
+ assurance.noteSession(r.key, `s-${i}`);
1493
+ const res = await settleSession({ key: r.key, ok: false, ...CAUSES[i], deps });
1494
+ if (res.spoke) sent++;
1495
+ }
1496
+ assert.equal(sent, CAUSES.length, "distinct causes are distinct sentences and every one is said");
1497
+ const texts = new Set(t.sent.filter((m) => m.kind === "failure").map((m) => m.text));
1498
+ assert.equal(texts.size, CAUSES.length);
1499
+ });
1500
+
1501
+ test("a sibling in a DIFFERENT room still hears it — the coalescing is per room", async () => {
1502
+ const t = fakeTransport();
1503
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1504
+ await openAndAcknowledge({ item: { ...ITEM, id: "r1-a", raw_ref: "cohort:r1:a" }, classResult: CLASS_COMPLEX, tier: "work" });
1505
+ await openAndAcknowledge({ item: { ...ITEM, id: "r1-b", raw_ref: "cohort:r1:b" }, classResult: CLASS_COMPLEX, tier: "work" });
1506
+ await openAndAcknowledge({
1507
+ item: { ...ITEM, id: "r2-a", raw_ref: "cohort:r2:a", channel_id: "C-OTHER-ROOM" },
1508
+ classResult: CLASS_COMPLEX, tier: "work",
1509
+ });
1510
+ await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 60_000, deps });
1511
+ const failures = t.sent.filter((m) => m.kind === "failure");
1512
+ assert.equal(failures.length, 2, "one per room");
1513
+ assert.equal(new Set(failures.map((m) => m.channel)).size, 2);
1514
+ });
1515
+
1516
+ test("a suppressed duplicate is still stamped on its own record, so it cannot leak out later", async () => {
1517
+ // If the record's own ledger did not learn the fact had been told in this
1518
+ // room, the next tick would re-say it the moment the room window rolled
1519
+ // over — turning suppression into a delay.
1520
+ const t = fakeTransport();
1521
+ const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false };
1522
+ const a = await openAndAcknowledge({ item: { ...ITEM, id: "st-a", raw_ref: "cohort:stamp:a" }, classResult: CLASS_COMPLEX, tier: "work" });
1523
+ const b = await openAndAcknowledge({ item: { ...ITEM, id: "st-b", raw_ref: "cohort:stamp:b" }, classResult: CLASS_COMPLEX, tier: "work" });
1524
+ assurance.noteSession(a.key, "s-a");
1525
+ assurance.noteSession(b.key, "s-b");
1526
+ await settleSession({ key: a.key, ok: false, code: 143, error: "SIGTERM", deps });
1527
+ await settleSession({ key: b.key, ok: false, code: 143, error: "SIGTERM", deps });
1528
+ assert.equal(t.sent.filter((m) => m.kind === "failure").length, 1);
1529
+ const recB = readObligation(b.key);
1530
+ assert.ok(Object.keys(recB.notices || {}).some((k) => k.startsWith(NOTICE.FAILURE_RETRYING)),
1531
+ "the suppressed record carries the stamp, so a later tick does not re-say it");
1532
+ });
1533
+ });
1534
+
1535
+ describe("the sweep is not re-entrant", () => {
1536
+ // agent-daemon.mjs fires sweepObligations() on a 60-second setInterval with no
1537
+ // in-flight guard of its own, and ONE TICK IS NOT BOUNDED BY 60 SECONDS:
1538
+ // branch (e) peeks the room budget with commit:false, then awaits a cold
1539
+ // `claude --print` spawn, serialised per obligation. A daemon with a dozen-plus
1540
+ // eligible obligations in distinct rooms spends more than a tick in generation
1541
+ // alone, so tick N+1 begins while tick N is still composing — and BOTH peek the
1542
+ // same room's budget and read "allowed", because the spend is only committed
1543
+ // after a successful send.
1544
+
1545
+ test("an overlapping sweep is refused rather than allowed to double-spend a room", async () => {
1546
+ const t = fakeTransport();
1547
+ await openAndAcknowledge({ item: { ...ITEM, id: "re-1", raw_ref: "cohort:reentry:1" }, classResult: CLASS_COMPLEX, tier: "work" });
1548
+ await openAndAcknowledge({ item: { ...ITEM, id: "re-2", raw_ref: "cohort:reentry:2" }, classResult: CLASS_COMPLEX, tier: "work" });
1549
+
1550
+ const now = Date.now() + ACK_AFTER_MS + 1000;
1551
+ let overlapped = null;
1552
+ // A generator that starts a SECOND sweep while the first is still awaiting
1553
+ // it — exactly the shape a slow `claude --print` produces on the timer.
1554
+ const deps = {
1555
+ deliverImpl: t.impl,
1556
+ spokeSinceImpl: () => false,
1557
+ generateAckImpl: async () => {
1558
+ if (overlapped === null) overlapped = await sweepObligations({ now, deps });
1559
+ return FAKE_ACK_LINE;
1560
+ },
1561
+ };
1562
+
1563
+ const first = await sweepObligations({ now, deps });
1564
+ assert.equal(overlapped && overlapped.skipped, true, "the re-entrant call was refused");
1565
+ assert.equal(overlapped.acked, 0, "…and it spoke to nobody");
1566
+ assert.equal(first.acked, 1, "the outer sweep still did its one room's work");
1567
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "one room, one interim, even across overlapping ticks");
1568
+ });
1569
+
1570
+ test("the guard is released even when the sweep throws, so it cannot disable the daemon", async () => {
1571
+ await openAndAcknowledge({ item: { ...ITEM, id: "thr-1", raw_ref: "cohort:throw:1" }, classResult: CLASS_COMPLEX, tier: "work" });
1572
+ const boom = {
1573
+ deliverImpl: async () => { throw new Error("transport exploded"); },
1574
+ spokeSinceImpl: () => false,
1575
+ generateAckImpl: async () => FAKE_ACK_LINE,
1576
+ };
1577
+ await assert.rejects(() => sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps: boom }));
1578
+ // The very next tick must run. Before the `finally`, this returned skipped.
1579
+ const after = await sweepObligations({
1580
+ now: Date.now() + ACK_AFTER_MS + 2000,
1581
+ deps: { deliverImpl: async () => ({ sent: true, via: "fake" }), spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1582
+ });
1583
+ assert.notEqual(after.skipped, true, "a throw must not leave the sweep disabled for the life of the daemon");
1584
+ });
1585
+ });
1586
+
1587
+ describe("the room-budget ledger is not an obligation", () => {
1588
+ // It lives at state/obligations/room-budget.json — the path the design names
1589
+ // — inside a directory whose reader JSON-parses every *.json as a debt record.
1590
+ // Parsed as one it was a phantom with no `key` and no `state`: it inflated
1591
+ // every count derived from listObligations(), and pruneObligations() called
1592
+ // unlinkSync(pathFor(undefined)) -> state/obligations/unknown.json on every
1593
+ // tick, swallowed by a catch. Harmless only because nothing can key to
1594
+ // "unknown" today.
1595
+ test("a sweep that spends a room's budget does not add an obligation to the ledger", async () => {
1596
+ const t = fakeTransport();
1597
+ await openAndAcknowledge({ item: { ...ITEM, id: "led-1", raw_ref: "cohort:ledger:1" }, classResult: CLASS_COMPLEX, tier: "work" });
1598
+ await sweepObligations({
1599
+ now: Date.now() + ACK_AFTER_MS + 1000,
1600
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1601
+ });
1602
+ const { existsSync } = await import("node:fs");
1603
+ assert.equal(existsSync(join(ROOT, "state", "obligations", "room-budget.json")), true, "the budget really was written there");
1604
+ const all = assurance.listObligations();
1605
+ assert.equal(all.length, 1, `one ask, one record — got ${all.length}`);
1606
+ assert.equal(all.every((r) => typeof r.key === "string" && r.key), true, "every record the reader returns is addressable");
1607
+ });
1608
+
1609
+ test("pruning never unlinks state/obligations/unknown.json", async () => {
1610
+ const { writeFileSync, existsSync } = await import("node:fs");
1611
+ const decoy = join(ROOT, "state", "obligations", "unknown.json");
1612
+ writeFileSync(decoy, JSON.stringify({ not: "an obligation" }));
1613
+ assurance.pruneObligations({ now: Date.now() + 10 * 24 * 3600_000 });
1614
+ assert.equal(existsSync(decoy), true, "a keyless record must never be resolved to a path and deleted");
1615
+ try { (await import("node:fs")).unlinkSync(decoy); } catch { /* cleanup only */ }
1616
+ });
1617
+ });
1618
+
1619
+ describe("notePlan — the plan tier says the plan, or says nothing", () => {
1620
+ const FIRST_TURN = {
1621
+ type: "assistant",
1622
+ message: { content: [{ type: "text", text: "1. Pull the July ledger rows and the signed forecast.\n2. Reconcile them line by line and mark every variance over 5%.\n3. Write up the three biggest gaps with the numbers behind each." }] },
1623
+ };
1624
+
1625
+ test("posts the plan the SESSION stated, once", async () => {
1626
+ const t = fakeTransport();
1627
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1628
+ const res = await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1629
+ assert.equal(res.spoke, true);
1630
+ assert.equal(t.sent.length, 1);
1631
+ assert.match(t.sent[0].text, /Pull the July ledger rows/);
1632
+ assert.match(t.sent[0].text, /three biggest gaps/);
1633
+ assert.equal(readObligation(r.key).interimSaid, true);
1634
+
1635
+ const again = await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1636
+ assert.equal(again.spoke, false);
1637
+ assert.equal(again.reason, "interim-already-said");
1638
+ assert.equal(t.sent.length, 1, "one message, and it is the plan");
1639
+ });
1640
+
1641
+ test("NEVER FABRICATES: no stated plan ⇒ nothing is posted", async () => {
1642
+ const t = fakeTransport();
1643
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1644
+ for (const turn of [null, undefined, "", "Sure, happy to look at that.", { type: "result" }, "{truncated"]) {
1645
+ const res = await notePlan({ key: r.key, firstTurn: turn, now: Date.now(), deps: { deliverImpl: t.impl } });
1646
+ assert.equal(res.spoke, false, `${JSON.stringify(turn)} must post nothing`);
1647
+ assert.equal(res.reason, "no-plan-stated");
1648
+ }
1649
+ assert.equal(t.sent.length, 0);
1650
+ assert.equal(readObligation(r.key).interimSaid, false, "and the latch is left unspent, so the sweep's one interim is still available");
1651
+ });
1652
+
1653
+ test("the ROOM budget outranks the plan tier too", async () => {
1654
+ const t = fakeTransport();
1655
+ const a = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1656
+ const b = await openAndAcknowledge({ item: { ...ITEM, id: "p2", raw_ref: "cohort:same-room:p2" }, classResult: CLASS_COMPLEX, tier: "plan" });
1657
+ assert.equal((await notePlan({ key: a.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } })).spoke, true);
1658
+ const second = await notePlan({ key: b.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1659
+ assert.equal(second.spoke, false);
1660
+ assert.equal(second.reason, "room-budget-spent");
1661
+ assert.equal(t.sent.length, 1);
1662
+ });
1663
+
1664
+ test("a plan-tier debt is NEVER silent, even though no caller supplies a first turn yet", async () => {
1665
+ // `notePlan` is exported and tested but not called by the daemon:
1666
+ // dispatcher.mjs spawns with `--output-format json` (not `stream-json`), so
1667
+ // the session's stdout arrives as one object at exit — after the answer has
1668
+ // gone out. Streaming is a dispatcher change and belongs with WP-2.
1669
+ //
1670
+ // What must hold in the meantime is that the plan tier degrades to the WORK
1671
+ // path rather than to silence. The sweep gates on `tier !== "answer"`, so it
1672
+ // does; this pins it, because a tier that mutes a 15-45 minute session is
1673
+ // the one failure this whole package exists to end.
1674
+ const t = fakeTransport();
1675
+ const r = await openAndAcknowledge({ item: { ...ITEM, id: "plan-fb", raw_ref: "cohort:planfb:1" }, classResult: CLASS_COMPLEX, tier: "plan" });
1676
+ const stats = await sweepObligations({
1677
+ now: Date.now() + ACK_AFTER_MS + 1000,
1678
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1679
+ });
1680
+ assert.equal(stats.acked, 1, "the plan tier falls back to the one interim, not to nothing");
1681
+ assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1);
1682
+ assert.equal(readObligation(r.key).interimSaid, true, "…and it is still only one");
1683
+ });
1684
+
1685
+ test("a work-tier or answer-tier debt is never given a plan", async () => {
1686
+ const t = fakeTransport();
1687
+ for (const tier of ["work", "answer"]) {
1688
+ const r = await openAndAcknowledge({ item: { ...ITEM, id: `t-${tier}`, raw_ref: `cohort:tier:${tier}` }, classResult: CLASS_COMPLEX, tier });
1689
+ const res = await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1690
+ assert.equal(res.spoke, false);
1691
+ assert.equal(res.reason, `tier-${tier}`);
1692
+ }
1693
+ assert.equal(t.sent.length, 0);
1694
+ });
1695
+
1696
+ test("a plan posted, then the session answers — the debt discharges with no extra word", async () => {
1697
+ const t = fakeTransport();
1698
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1699
+ await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1700
+ const before = t.sent.length;
1701
+ const settled = await settleSession({ key: r.key, ok: true, code: 0, deps: { deliverImpl: t.impl, spokeForImpl: () => ({ heard: true, basis: "receipt" }) } });
1702
+ assert.equal(settled.verdict, "answered-by-session");
1703
+ assert.equal(t.sent.length, before, "the daemon adds nothing on top of the session's own answer");
1704
+ });
1705
+ });
1706
+
1707
+ describe("COPY: no promise without a mechanism", async () => {
1708
+ // §5.1.5. An interim may not contain a future-tense commitment unless a
1709
+ // DURABLE obligation exists and the sweep can close it. The two deleted
1710
+ // progress lines broke this twice over: they promised ("I'll come back as
1711
+ // soon as I've got something") from a timer that had no idea whether anything
1712
+ // would ever come back, and 272 acknowledgements proved the promise empty.
1713
+ // IMPORTED, never re-typed. The sibling test one describe-block up exists to
1714
+ // stop the ack prompt and its filter drifting apart; a copy test that
1715
+ // re-declares the pattern it is checking has the identical defect. The local
1716
+ // copy this replaces had already drifted: it was missing the `revert`
1717
+ // alternative and the whole `i will (come back|...)` branch, so
1718
+ // "I will report back once the reconciliation is done" was dropped by the
1719
+ // implementation and would have PASSED the test's own check.
1720
+ const { FORWARD_PROMISE } = await import("../../lib/assurance/plan-note.mjs");
1721
+
1722
+ test("the deleted progress lines are gone from the module entirely", () => {
1723
+ assert.equal(typeof assurance.composeProgress, "undefined", "composeProgress may not be reintroduced");
1724
+ assert.equal(typeof assurance.PROGRESS_AFTER_MS, "undefined");
1725
+ assert.equal(typeof assurance.PROGRESS_EVERY_MS, "undefined");
1726
+ assert.equal(typeof assurance.PROGRESS_MAX, "undefined");
1727
+ });
1728
+
1729
+ test("…and the retired strings are still RECOGNISED, so a peer running older code cannot restart the loop", () => {
1730
+ for (const line of RETIRED_NARRATOR_TEMPLATES) {
1731
+ assert.equal(isOwnNarratorText(line), true, `"${line}" must still be recognised as machinery talking`);
1732
+ }
1733
+ });
1734
+
1735
+ test("every interim the system can emit is promise-free", async () => {
1736
+ // The plan note is the only composed interim left; the other is the
1737
+ // model-generated ack, which the sanitiser governs. Both are checked.
1738
+ const { renderPlanNote } = await import("../../lib/assurance/plan-note.mjs");
1739
+ const note = renderPlanNote("1. Pull the ledger rows for July.\n2. Reconcile them against the signed forecast.\n3. I'll come back as soon as I've got something.");
1740
+ assert.ok(note);
1741
+ assert.doesNotMatch(note, FORWARD_PROMISE, "a plan bullet carries no durable backing and may not promise a return");
1742
+
1743
+ // A generated ack that promises is still admissible ONLY because a durable
1744
+ // obligation is what carries it — pinned here so the coupling is explicit.
1745
+ const promising = "Starting on the July reconciliation now; I'll come back with the variances.";
1746
+ assert.equal(sanitiseAckText(promising), promising);
1747
+ const t = fakeTransport();
1748
+ const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1749
+ await sweepObligations({
1750
+ now: Date.now() + ACK_AFTER_MS + 1000,
1751
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => promising },
1752
+ });
1753
+ assert.equal(t.sent.length, 1);
1754
+ const rec = readObligation(r.key);
1755
+ assert.equal(rec.state, "open", "the promise is backed: the debt is durable…");
1756
+ assert.equal(openObligations().some((o) => o.key === r.key), true, "…and the sweep can still find it and close it");
1757
+ });
1758
+
1759
+ test("an obligation the sweep CANNOT close never gets to promise anything", async () => {
1760
+ // No transport ⇒ no interim, at open time or ever. A promise into a channel
1761
+ // that does not exist is the purest form of the broken-promise failure.
1762
+ const t = fakeTransport();
1763
+ const TELEGRAM_ITEM = { ...ITEM, id: "TG-9", raw_ref: "telegram:9", service: "telegram", channel: null, channel_id: null };
1764
+ const r = await openAndAcknowledge({ item: TELEGRAM_ITEM, classResult: CLASS_COMPLEX, tier: "work", ack: false });
1765
+ assert.equal(readObligation(r.key).interimForbidden, true);
1766
+ await sweepObligations({
1767
+ now: Date.now() + ACK_AFTER_MS + 1000,
1768
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => "I'll come back as soon as I've got something." },
1769
+ });
1770
+ assert.equal(t.sent.length, 0);
1771
+ });
1772
+ });
1773
+
1774
+ describe("the answer tier is silent even when a caller gets the flag wrong", () => {
1775
+ test("a record whose TIER is answer never produces an interim, ack:true or not", async () => {
1776
+ // Defence in depth. `shouldAcknowledge` returns ack:false for the answer
1777
+ // tier and the daemon turns that into `interimForbidden`, but the tier is
1778
+ // the durable fact on the record and is what "never speak about this one"
1779
+ // actually means. A caller that passes the tier and forgets the flag must
1780
+ // not be able to re-open the flood.
1781
+ const t = fakeTransport();
1782
+ await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "answer", ack: true });
1783
+ assert.equal(readObligation(ITEM.raw_ref).interimForbidden, undefined, "the flag was NOT set — this is the caller bug being defended against");
1784
+ const stats = await sweepObligations({
1785
+ now: Date.now() + ACK_AFTER_MS + 60_000,
1786
+ deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1787
+ });
1788
+ assert.equal(stats.acked, 0);
1789
+ assert.equal(t.sent.length, 0, "the tier alone is enough to keep it quiet");
1790
+ });
1791
+ });