@sema-agent/core 7.35.1 → 7.35.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 7.35.2 — 2026-09-29
2
+
3
+ ### A wait the watchdog cuts at the 30 s disclosure threshold is disclosed before the cut (#746; behavior (status frames: +1 `waiting_first_token` frame per attempt when a first-token or first-byte window is exactly the 30 s threshold); +0 exports, +0 wire keys, +0 codes, closed sets unchanged) (@cli @client-core @server @test)
4
+
5
+ - **Before (v7.35.1).** Measured by cli against a running engine (L-778, five scenarios: a gateway that sends headers at once and its first token 45 s later, or holds its headers 45 s). With the first-token watchdog at 30 s (`firstTokenTimeoutMs`; server env `MODEL_FIRST_TOKEN_TIMEOUT_MS`), a request waited 30 s with no `status` frame at all and was then cut. The wait disclosure was armed only for a ceiling strictly above its 30 s threshold, so the one tick that fell on the deadline was never sent. At 40 s, or at the 600 s default, the 30 s frame was there. A first-byte window of exactly 30 s (knob plus upload allowance) had the same gap.
6
+ - **Now.** The wait disclosure is armed whenever its ceiling reaches the threshold (`>=`, the watchdog's own comparison). When the watchdog fires, a wait not yet disclosed is disclosed at that instant — `waiting_first_token` with `elapsedMs` equal to `timeoutMs` — and the retry (or terminal) frame follows. The deadline owns that frame, so which timer runs first no longer decides whether it is sent. This holds for the first-token wait and the header wait alike. Re-announcements are unchanged: they stay strictly before the ceiling, so a 120 s watchdog still shows 30 / 60 / 90 s and no frame at 120 s.
7
+ - **Not changed.** The retry after a watchdog cut keeps `phase: "reconnecting"` with `errClass: "stall"`. The shells already tell the two apart by `errClass`: cli L-264 renders it as "No output before the stream timeout · slow model or gateway", through client-core's `stalled` arm. The engine does not rename the phase. The header wait and the first-token wait still share `waiting_first_token`; a separate phase word would be a wire change.
8
+ - **Readings (engine fixtures of cli's scenarios, fake clock).**
9
+ - A (token 45 s, watchdog 30 s, every request slow): 0 → 11 wait frames, one per attempt, each 30000 of 30000, each before its attempt's `reconnecting` / `stall` frame. The call still ends `gave_up` / `first-token timeout after 30000ms`.
10
+ - A2 (only the first request slow): `reconnecting, recovered` → `waiting_first_token, reconnecting, recovered`.
11
+ - B′ (header window exactly 30 s): the same.
12
+ - B (headers 45 s, first-byte 30 s + 3 s upload allowance), C (the 600 s default) and D (40 s): unchanged.
13
+ - **Who is hurt.** A deployment whose first-token (or first-byte) window is exactly 30 s sees one more frame per attempt, at the deadline. The frame is closed by `recovered` / `gave_up` as before. client-core maps it to `kind: 'waiting'` and cli renders its waiting byline. A probe that counted zero wait frames under a 30 s watchdog goes red. The engine defaults are unchanged.
14
+ - Pins: `test/backlog746-first-token-watchdog-phase.test.ts`:
15
+ - A ① every attempt's wait is disclosed at its deadline; A ② control: the retries and the end; A ③ an event loop stalled across the deadline still gets exactly one wait frame before the retry.
16
+ - A2, B′ (the full trails), B, C, D (controls).
17
+ - Baseline v7.35.1: A ①, A ③, A2 and B′ red; A ②, B, C and D green.
18
+ - Unchanged and green: `backlog530-waiting-first-token`, including the 120 s cell and the stalled-event-loop cell.
19
+ - Mutations:
20
+ - D960, the disclosure arms only above the threshold (v7.35.1's `>`): A ①, A ③, A2 and B′ go red.
21
+ - D960b, the deadline does not disclose (`expire` does what `stop` does): the same four go red. Per site: the first-token watchdog calling `stop` turns A ①, A ③ and A2 red; the header `finally` always stopping turns B′ red.
22
+ - D960c, the deadline discloses a wait that was already disclosed: the controls go red (D, B, and `backlog530` ④ / ⑥ / ④c).
23
+ - Per-fixture table: `.gate-logs/criteria-7.35.2-mutations.md`.
24
+
1
25
  ## 7.35.1 — 2026-09-29
2
26
 
3
27
  ### A run that named no session holds the session its store minted — a second run on that id waits for it (#1206; behavior (tighten: a second run of a new session queues); +0 exports, +0 wire keys; +1 engine-invariant code `internal.session_lock_second_id` that no engine path reaches) (@server @test)
@@ -96,7 +96,8 @@ export declare function shouldRetryHeaderVerdict(res: Response | undefined): boo
96
96
  * {@link WAITING_FIRST_TOKEN_AFTER_MS}, and one more every {@link WAITING_FIRST_TOKEN_EVERY_MS} after
97
97
  * that, until the first token lands, the watchdog fires, or the attempt ends. A short wait (under the
98
98
  * threshold) announces nothing — the frame exists for the unattended "no answer for a minute" case,
99
- * not for every request. The same 30s slice the retry countdown re-announces on.
99
+ * not for every request. A wait whose watchdog fires AT the threshold is disclosed once, at that
100
+ * instant, before the cut is announced. The same 30s slice the retry countdown re-announces on.
100
101
  */
101
102
  export declare const WAITING_FIRST_TOKEN_AFTER_MS = 30000;
102
103
  export declare const WAITING_FIRST_TOKEN_EVERY_MS = 30000;
@@ -337,29 +337,40 @@ export function runStreamingBrain(args) {
337
337
  const idleTimeoutMs = resolveWatchdogMs(config.idleTimeoutMs, stallTimeouts?.idleMs, STALL_IDLE_MS, "idleTimeoutMs");
338
338
  const firstByteRetryBudget = resolveFirstByteRetries(config.firstByteRetries);
339
339
  const armWaitDisclosure = (ceilingMs) => {
340
- if (!(ceilingMs > WAITING_FIRST_TOKEN_AFTER_MS))
341
- return { stop: () => undefined };
340
+ if (!(ceilingMs >= WAITING_FIRST_TOKEN_AFTER_MS))
341
+ return { stop: () => undefined, expire: () => undefined };
342
342
  const startedAt = Date.now();
343
343
  const emitWaitFrame = bindBrainStatusEmitter();
344
344
  let timer;
345
345
  let stopped = false;
346
+ let disclosed = false;
347
+ const disclose = () => {
348
+ disclosed = true;
349
+ announcedWait = true;
350
+ emitWaitFrame({ phase: "waiting_first_token", detail: "waiting for the first token", elapsedMs: Date.now() - startedAt, timeoutMs: ceilingMs });
351
+ };
346
352
  const schedule = (delayMs) => {
347
353
  timer = setTimeout(() => {
348
354
  timer = undefined;
349
355
  if (stopped || Date.now() - startedAt >= ceilingMs)
350
356
  return;
351
- announcedWait = true;
352
- emitWaitFrame({ phase: "waiting_first_token", detail: "waiting for the first token", elapsedMs: Date.now() - startedAt, timeoutMs: ceilingMs });
357
+ disclose();
353
358
  schedule(WAITING_FIRST_TOKEN_EVERY_MS);
354
359
  }, delayMs);
355
360
  };
356
361
  schedule(WAITING_FIRST_TOKEN_AFTER_MS);
362
+ const stop = () => {
363
+ stopped = true;
364
+ if (timer)
365
+ clearTimeout(timer);
366
+ timer = undefined;
367
+ };
357
368
  return {
358
- stop: () => {
359
- stopped = true;
360
- if (timer)
361
- clearTimeout(timer);
362
- timer = undefined;
369
+ stop,
370
+ expire: () => {
371
+ if (!stopped && !disclosed)
372
+ disclose();
373
+ stop();
363
374
  },
364
375
  };
365
376
  };
@@ -378,13 +389,13 @@ export function runStreamingBrain(args) {
378
389
  throw e;
379
390
  }
380
391
  const window = firstByteWindow(firstByteTimeoutMs, Buffer.byteLength(req.body), firstByteStalls);
392
+ const headerWait = armWaitDisclosure(window.windowMs);
381
393
  const cc = createAttemptController(window.windowMs, signal);
382
394
  let r;
383
395
  let netErr;
384
396
  let firstByteStall;
385
397
  requestIdSeen = undefined;
386
398
  attemptPartial = undefined;
387
- const headerWait = armWaitDisclosure(window.windowMs);
388
399
  const stalledAtFirstByte = () => {
389
400
  firstByteWindowsWaited.push(window.windowMs);
390
401
  return {
@@ -423,7 +434,10 @@ export function runStreamingBrain(args) {
423
434
  }
424
435
  }
425
436
  finally {
426
- headerWait.stop();
437
+ if (firstByteStall !== undefined)
438
+ headerWait.expire();
439
+ else
440
+ headerWait.stop();
427
441
  }
428
442
  if (r && r.ok && r.body) {
429
443
  cc.clearTimer();
@@ -587,14 +601,14 @@ export function runStreamingBrain(args) {
587
601
  let firstTokenSeen = false;
588
602
  let firedFirstTokenDeadlineMs;
589
603
  let ftTimer;
590
- let firstTokenWait = { stop: () => undefined };
604
+ let firstTokenWait = { stop: () => undefined, expire: () => undefined };
591
605
  if (firstTokenTimeoutMs && firstTokenTimeoutMs > 0) {
606
+ firstTokenWait = armWaitDisclosure(firstTokenTimeoutMs);
592
607
  ftTimer = setTimeout(() => {
593
608
  firedFirstTokenDeadlineMs = firstTokenTimeoutMs;
594
- firstTokenWait.stop();
609
+ firstTokenWait.expire();
595
610
  void reader.cancel().catch(() => undefined);
596
611
  }, firstTokenTimeoutMs);
597
- firstTokenWait = armWaitDisclosure(firstTokenTimeoutMs);
598
612
  }
599
613
  let firedIdleDeadlineMs;
600
614
  let idleTimer;
@@ -78,7 +78,8 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
78
78
  * The first-token WAIT, before any failure: a first-token watchdog is armed
79
79
  * (`firstTokenTimeoutMs > 0`) and nothing has arrived yet. Emitted once the wait has lasted
80
80
  * `WAITING_FIRST_TOKEN_AFTER_MS` and re-emitted every `WAITING_FIRST_TOKEN_EVERY_MS` while it goes
81
- * on, carrying {@link BrainStatus.elapsedMs} and {@link BrainStatus.timeoutMs}; never carries a
81
+ * on; a wait whose watchdog fires at the threshold itself is emitted once at that instant, before the
82
+ * frame announcing the cut. Carries {@link BrainStatus.elapsedMs} and {@link BrainStatus.timeoutMs}; never carries a
82
83
  * retry countdown (`retryInMs`/`retryAtMs` — no wait is being announced, a wait is being
83
84
  * OBSERVED). CLOSED like a retry trail: once a wait frame went out, the call's terminal frame
84
85
  * follows — `recovered` when the call went on to be served (the first token arrived, on this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.35.1",
3
+ "version": "7.35.2",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -0,0 +1,61 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.35.2",
4
+ "from": "47ed594ee8d729368c4be8d8a189d908b33e15e5",
5
+ "to": "v7.35.2",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [],
13
+ "typeKeysUnreadableArms": [],
14
+ "typeKeysFlattenedCount": 219,
15
+ "declarationMoves": [],
16
+ "mintMoves": [],
17
+ "criteriaObserves": [
18
+ {
19
+ "kind": "typeMember",
20
+ "name": "BrainStatus.elapsedMs",
21
+ "assertions": [
22
+ "G1.a",
23
+ "G1.c"
24
+ ]
25
+ },
26
+ {
27
+ "kind": "typeMember",
28
+ "name": "BrainStatus.errClass",
29
+ "assertions": [
30
+ "G1.b"
31
+ ]
32
+ },
33
+ {
34
+ "kind": "typeMember",
35
+ "name": "BrainStatus.phase",
36
+ "assertions": [
37
+ "G1.a",
38
+ "G1.b",
39
+ "G1.c"
40
+ ]
41
+ },
42
+ {
43
+ "kind": "typeMember",
44
+ "name": "BrainStatus.timeoutMs",
45
+ "assertions": [
46
+ "G1.a",
47
+ "G1.b",
48
+ "G1.c"
49
+ ]
50
+ }
51
+ ],
52
+ "consumersHint": {
53
+ "cli": [],
54
+ "server": [],
55
+ "sdk": [],
56
+ "note": "scanned cli@6b6025f7 server@25d6b7a6 sdk@e8f6f0d — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
57
+ },
58
+ "breakingCount": 0,
59
+ "breakingItems": [],
60
+ "toResolvedAt": "8a69688057ed91863f6574e06b9f4f83dfdcb7a6"
61
+ }