@cohortapp/agent-sdk 2.14.0 → 2.16.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.
@@ -207,10 +207,42 @@ test("SCENARIO 1 — a fast ask gets a direct answer, with no pointless holding
207
207
  });
208
208
 
209
209
  // ===========================================================================
210
- // SCENARIO 2 — A SLOW ASK. Acknowledged in milliseconds, updated, then answered.
210
+ // SCENARIO 2 — A SLOW ASK. Silent for ninety seconds, one line, then answered.
211
+ //
212
+ // WHY THIS SCENARIO CHANGED ON 2026-09-12, AND WHY THE PROGRESS PING IS GONE
213
+ //
214
+ // It used to assert an acknowledgement at t≈0 and EXACTLY ONE progress ping at
215
+ // six minutes. Both assertions were faithful to the code and both encoded the
216
+ // defect that code had become.
217
+ //
218
+ // Measured over fourteen days in org_default_adaptic: 10,667 agent messages, of
219
+ // which 3,069 (28.8%) were opening acknowledgements and 961 (9.0%) were progress
220
+ // nags — 37.8% of everything this fleet said carried no content at all. 272 of
221
+ // those acknowledgements were never followed by a substantive reply inside an
222
+ // hour, i.e. the promise in them ("I'll come back as soon as I've got
223
+ // something") was simply not kept. Agent-to-human volume went from 2:1 to 158:1
224
+ // in a fortnight, and 9,187 of ~11,400 messages in 21 days landed in ONE
225
+ // channel from eight agents, ~45% of them exact duplicates. Sampled live at
226
+ // 06:32Z: "Still on this — 5 minutes in" beside "Still going — 15 minutes in"
227
+ // at the SAME timestamp, two independent per-obligation timers narrating one
228
+ // piece of overlapping work.
229
+ //
230
+ // The progress ping could not have been better written. `composeProgress` was a
231
+ // pure function of `now - openedAt`; nothing read the session's stdout, its
232
+ // tool calls or its partial findings, so the message could not contain anything
233
+ // a reader could not already see on the clock. A message that says only what
234
+ // the timestamp says is not an update. It is deleted, and this scenario now
235
+ // asserts that NONE is emitted — not because the assertion was wrong about the
236
+ // code, but because the behaviour it pinned was the bug.
237
+ //
238
+ // What replaces it: silence below ACK_AFTER_MS (the typing indicator is the
239
+ // acknowledgement), then AT MOST ONE bespoke line, and only if the room has not
240
+ // already heard one inside its budget window. Beyond that the agent either says
241
+ // something with content in it — the plan the session itself stated — or it says
242
+ // nothing and lets the answer be the answer.
211
243
  // ===========================================================================
212
244
 
213
- test("SCENARIO 2 — a slow ask is acknowledged at once, updated while it runs, then answered", async () => {
245
+ test("SCENARIO 2 — a slow ask is silent, then gets ONE line, then the answer — and no progress nag", async () => {
214
246
  const ask = "with that in mind, fix these issues you just noted please and push their fixes to git";
215
247
  beginScenario("SCENARIO 2 — SLOW ASK (the owner's actual message)", ask);
216
248
  const item = makeItem("SLOW-1", ask);
@@ -224,23 +256,45 @@ test("SCENARIO 2 — a slow ask is acknowledged at once, updated while it runs,
224
256
  dispatch: (_p, _it, _cr, _s, opts) => { onCloseHook = opts.onClose; },
225
257
  });
226
258
 
227
- const ack = transcript.find((m) => m.kind === "ack");
228
- assert.ok(ack, "the acknowledgement must go out");
229
- assert.ok(ack.ms < 1000, `acknowledged in ${ack.ms}ms`);
230
- assert.match(ack.text, /git fixes/i, "the ack is bespoke to the actual ask, not a rotation pick");
231
- assert.doesNotMatch(ack.text, /^(on it|looking now)[.!—]?\s*$/i, "never the retired canned lines");
232
- assert.ok(ack.text.length <= 120, "the ack is one short human line, not a templated paragraph");
233
-
259
+ assert.equal(transcript.length, 0, "NOTHING at t=0 — the reflex ack is what 3,069 content-free messages were");
234
260
  const key = assurance.openObligations()[0].key;
235
- note("obligation opened and acknowledged; session running");
261
+ assert.equal(assurance.readObligation(key).state, "open", "the DEBT is opened all the same: silence about timing is not silence about outcome");
262
+ note("obligation opened, nothing said; session running");
236
263
 
237
- // ── 6 minutes later: still working. The sweep speaks rather than let the
238
- // human sit behind a typing indicator wondering.
239
- let virtual = 6 * 60_000;
264
+ // ── Thirty seconds in: still nothing. Below ACK_AFTER_MS the typing
265
+ // indicator is the acknowledgement.
266
+ let virtual = 30_000;
240
267
  const clock = () => virtual;
241
- const deps = { deliverImpl: transport(clock), spokeSinceImpl: () => false };
242
- let stats = await assurance.sweepObligations({ now: Date.now() + virtual, deps });
243
- assert.equal(stats.progressed, 1, "long work must produce an interim update");
268
+ const deps = () => ({
269
+ deliverImpl: transport(clock),
270
+ spokeSinceImpl: () => false,
271
+ generateAckImpl: async () => "Taking the git fixes now — push coming.",
272
+ });
273
+ let stats = await assurance.sweepObligations({ now: Date.now() + virtual, deps: deps() });
274
+ assert.equal(stats.acked, 0);
275
+ assert.equal(transcript.length, 0, "under ninety seconds, silence is the right answer");
276
+
277
+ // ── Two minutes in: ONE line, specific to the ask.
278
+ virtual = 2 * 60_000;
279
+ stats = await assurance.sweepObligations({ now: Date.now() + virtual, deps: deps() });
280
+ assert.equal(stats.acked, 1, "past ACK_AFTER_MS the human gets exactly one line");
281
+ const ack = transcript.find((m) => m.kind === "ack");
282
+ assert.ok(ack, "…and it is that line");
283
+ assert.match(ack.text, /git fixes/i, "bespoke to the actual ask, not a rotation pick");
284
+ assert.doesNotMatch(ack.text, /^(on it|looking now|checking|one moment|got it|will do)\b/i, "never a generic opener — the sanitiser now enforces the prompt");
285
+ assert.ok(ack.text.length <= 120, "one short human line, not a templated paragraph");
286
+
287
+ // ── Six minutes in — where the progress ping used to land. NOTHING.
288
+ virtual = 6 * 60_000;
289
+ stats = await assurance.sweepObligations({ now: Date.now() + virtual, deps: deps() });
290
+ assert.equal(stats.acked, 0);
291
+ const progressish = transcript.filter((m) => /still (on this|going)/i.test(m.text));
292
+ assert.equal(progressish.length, 0, "a message that is a pure function of elapsed minutes cannot carry progress");
293
+
294
+ // ── Sixteen minutes: and still nothing, however long it runs.
295
+ virtual = 16 * 60_000;
296
+ await assurance.sweepObligations({ now: Date.now() + virtual, deps: deps() });
297
+ assert.equal(transcript.length, 1, `the whole wait costs the human ONE message, got ${transcript.length}`);
244
298
 
245
299
  // ── 17 minutes: the session finally answers the human itself.
246
300
  virtual = 17 * 60_000;
@@ -251,9 +305,8 @@ test("SCENARIO 2 — a slow ask is acknowledged at once, updated while it runs,
251
305
 
252
306
  const rec = assurance.readObligation(key);
253
307
  assert.equal(rec.state, "answered", "the debt is discharged by evidence of a delivered reply");
254
- const progress = transcript.filter((m) => m.kind === "progress");
255
- assert.equal(progress.length, 1);
256
308
  assert.equal(transcript.filter((m) => m.kind === "ack").length, 1, "acknowledge once, never twice");
309
+ assert.equal(transcript.filter((m) => m.kind === "progress").length, 0, "and never a progress ping — the branch that emitted them is deleted");
257
310
  note("obligation discharged by receipt — the daemon added nothing on top of the session's own answer", virtual);
258
311
  });
259
312
 
@@ -275,7 +328,10 @@ test("SCENARIO 3 — a timing-out session tells the human what happened and that
275
328
  dispatch: (_p, _it, _cr, _s, opts) => { onCloseHook = opts.onClose; },
276
329
  });
277
330
  const key = assurance.openObligations().find((o) => o.key === item.raw_ref).key;
278
- assert.ok(transcript.find((m) => m.kind === "ack"));
331
+ // Nothing has been said yet — the ask is 45 minutes old in VIRTUAL time only,
332
+ // and the sweep has not run. What matters for this scenario is that the debt
333
+ // exists, so the failure has somewhere to be reported from.
334
+ assert.equal(transcript.length, 0, "silence is the default; the failure notice below is the first thing this human hears");
279
335
 
280
336
  // 45 minutes in, the dispatcher's SIGTERM lands and the session closes 143.
281
337
  const virtual = { v: 45 * 60_000 };