@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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
@@ -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
|
+
}
|