@sema-agent/core 5.40.0 → 5.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -3
- package/dist/core/auto-mode-prompt.js +1 -1
- package/dist/core/memory-engine/file-backend.js +11 -0
- package/dist/core/memory-engine/layout.d.ts +10 -0
- package/dist/core/memory-engine/layout.js +12 -6
- package/dist/core/runner/prepare-config-doors.js +20 -0
- package/dist/core/runner/runtask.js +9 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/engine/harness/agent-harness.d.ts +17 -0
- package/dist/engine/harness/agent-harness.js +19 -1
- package/dist/engine/harness/types.d.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 5.
|
|
3
|
+
## 5.41.0 — 2026-08-18
|
|
4
4
|
|
|
5
|
-
No BREAKING changes.
|
|
5
|
+
No BREAKING changes. Three behavioral narrowings disclosed below (garbage values on the three
|
|
6
|
+
tool-face seats now refuse loudly; the strict sidecar family's journal read and lock fence both
|
|
7
|
+
join the fail-closed law).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- backlog #259 — user steers stranded at agent_end are ANNOUNCED, never silently dropped behind
|
|
12
|
+
their "queued" receipts: the terminal sweep hands ENGINE notes back for per-session redelivery,
|
|
13
|
+
but USER inputs have no redelivery semantics (a steer aimed at a finished run must not fire at
|
|
14
|
+
the next one) — their remnant counts now ride a dedicated harness sink, the `settled` frame
|
|
15
|
+
(additive `undrainedSteerCount`/`undrainedFollowUpCount`), and a new `EngineNotice` family
|
|
16
|
+
`task.user_steer_undrained`. The stranding window is a narrow race past the loop's final queue
|
|
17
|
+
check, which is exactly why it gets a loud terminal account.
|
|
18
|
+
- backlog #251 — the mixed-version window of the design/186 ledger v2 is machine-detected:
|
|
19
|
+
recovering a v1-format journal over a v2 disk ledger (a shape only a pre-v2 process mints)
|
|
20
|
+
announces the old-writer-alive warning on the external-findings channel; recovery is unchanged.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- backlog #297 (ruled: narrow) — the three tool-face seats (`excludeTools` / `deferTools` /
|
|
25
|
+
`alwaysLoadTools`) get the fourth seat's value door: a garbage value (a bare string, a truthy
|
|
26
|
+
non-iterable, a non-string entry) refuses `config.tool_face_control` instead of stringifying
|
|
27
|
+
into a never-matching list, throwing an uncoded TypeError, or a falsy value silently reading as
|
|
28
|
+
absent. **Narrowing**: a deployment whose misspelled seat previously "worked" now hears it.
|
|
29
|
+
- backlog #299 (ruled: fix alone) — the auto-mode classifier's action block bounds tool args at
|
|
30
|
+
48,000 chars (its own subject-level cap, deliberately above the delegation-review lanes'
|
|
31
|
+
12,000-per-field sampled payloads so their coverage notes ride through uncut): it was the one
|
|
32
|
+
unbounded string in the classifier's prompt, so a single long tool argument (a SendMessage body
|
|
33
|
+
needs nothing crafted, just length) could blow the classifier's context and trip the session
|
|
34
|
+
breaker out of auto mode.
|
|
35
|
+
- backlog #307 (also filed as #256) — the journal-aware strict sidecar read joins the ENOENT-only
|
|
36
|
+
absence law: an EACCES/EIO journal read refused `ControlPlaneCorruptError` instead of silently
|
|
37
|
+
serving the pre-transaction state while a committed next state sat unreadable beside it — the
|
|
38
|
+
one fail-open left in the strict-read family. **Narrowing**: a deployment whose journal reads
|
|
39
|
+
were failing now hears it instead of silently reading stale.
|
|
40
|
+
- backlog #255 — the sidecar lock's commit fence splits by family: the fail-closed LEDGER family
|
|
41
|
+
(challenge/lineage locked updates and the rebuild path) aborts on an UNPROVABLE owner
|
|
42
|
+
(absent/unreadable owner file) — "cannot disprove a steal" is not a fence; the calibration
|
|
43
|
+
family keeps its documented pre-token leniency, and a FOREIGN token aborts both as before.
|
|
44
|
+
**Narrowing**: within the ledger family only.
|
|
45
|
+
- CHANGELOG erratum for 5.40.0 (recorded in that entry): the header counts THREE narrowings
|
|
46
|
+
(the defer alignment was the missing third) and the release date corrects to 08-17.
|
|
47
|
+
|
|
48
|
+
## 5.40.0 — 2026-08-17
|
|
49
|
+
|
|
50
|
+
No BREAKING changes. Three behavioral narrowings disclosed below (the requestedCwd × workspace-restore
|
|
6
51
|
combination now refuses loudly; Grep's ripgrep leg withholds alias-spelled deny-listed paths it
|
|
7
|
-
previously returned
|
|
52
|
+
previously returned; `deferMode: "auto"` never sweeps engine built-ins — a small-window deployment
|
|
53
|
+
whose built-ins were previously deferred now carries their schemas inline, trading context bytes for
|
|
54
|
+
the first-use activation error the sweep caused).
|
|
55
|
+
|
|
56
|
+
<!-- Post-release erratum (2026-08-17, server review [4351]): the header originally said "Two
|
|
57
|
+
behavioral narrowings" and dated the entry 2026-08-18 (pre-written); the defer narrowing was listed
|
|
58
|
+
under Fixed but missing from this header, and the release actually shipped late on 08-17. -->
|
|
8
59
|
|
|
9
60
|
### Added
|
|
10
61
|
|
|
@@ -102,5 +102,5 @@ export function renderAutoModeWindow(messages, options) {
|
|
|
102
102
|
export function renderAutoModeAction(input) {
|
|
103
103
|
const ask = input.askMessage ? `\npermission gate: ${input.askMessage}` : "";
|
|
104
104
|
return (`\n## New action to classify (the agent's most recent action — evaluate THIS)\n\n` +
|
|
105
|
-
`[tool_call] ${input.req.toolName} ${JSON.stringify(input.req.args ?? {})}${ask}\n`);
|
|
105
|
+
`[tool_call] ${input.req.toolName} ${excerpt(JSON.stringify(input.req.args ?? {}), 48_000)}${ask}\n`);
|
|
106
106
|
}
|
|
@@ -1166,6 +1166,17 @@ export class FileMemoryEngineBackend {
|
|
|
1166
1166
|
throw new ControlPlaneCorruptError(`transaction journal v1 ledger snapshot entry ${JSON.stringify(id)} is not a string: ${jp}`);
|
|
1167
1167
|
snapshotRows[id] = { rev };
|
|
1168
1168
|
}
|
|
1169
|
+
try {
|
|
1170
|
+
const diskRaw = readControlFileOrAbsent(join(this.controlPlaneRoot, LEDGER_FILE), "committed-rev ledger");
|
|
1171
|
+
const diskParsed = diskRaw !== undefined ? JSON.parse(diskRaw) : undefined;
|
|
1172
|
+
if (diskParsed !== null && typeof diskParsed === "object" && "v" in diskParsed && diskParsed.v === LEDGER_SCHEMA_VERSION) {
|
|
1173
|
+
this.enqueueExternalItems([
|
|
1174
|
+
"memory ledger: recovering a v1-format journal over a v2 ledger — a pre-v2 process wrote AFTER this store migrated (mixed-version deployment window). Retire old readers/writers before further writes; see the v2 rollback runbook.",
|
|
1175
|
+
]);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
}
|
|
1169
1180
|
}
|
|
1170
1181
|
for (const op of journal.ops) {
|
|
1171
1182
|
const invalid = journalOpInvalid(op, this.directoryRoot);
|
|
@@ -248,6 +248,16 @@ export interface SidecarLockOptions {
|
|
|
248
248
|
* volume, revoked permission) makes mkdir fail forever while the lock dir it would stat never
|
|
249
249
|
* exists — spun this synchronous loop at full speed and never reached its own cap. */
|
|
250
250
|
export declare function acquireSidecarLock(lockDir: string, opts?: SidecarLockOptions): string;
|
|
251
|
+
/** 证伪式复审 L2-analog: re-verify the owner token at the COMMIT POINT — a stolen-from holder
|
|
252
|
+
* aborts instead of writing over the stealer. A FOREIGN token always aborts (positive proof the
|
|
253
|
+
* lock is someone else's now). What an ABSENT/UNREADABLE owner file means splits by caller family
|
|
254
|
+
* (backlog #255): the fail-closed LEDGER family (`"strict"` — challenges/lineage via
|
|
255
|
+
* lockedStrictUpdate) treats unprovable ownership as lost — those sidecars promise their commit
|
|
256
|
+
* point is fenced, and "cannot disprove" is not a fence; the fail-open calibration family
|
|
257
|
+
* (`"lenient"`, the pre-#255 shape — e.g. the scope registry, whose sidecars self-heal at the next
|
|
258
|
+
* locked update) keeps the leniency, because its token writes are best-effort and an absent owner
|
|
259
|
+
* file there is the documented pre-token degraded shape, not evidence of a steal. */
|
|
260
|
+
export declare function assertSidecarLockOwnership(lockDir: string, token: string, what: string, mode?: "strict" | "lenient"): void;
|
|
251
261
|
interface AnnouncementsRecord {
|
|
252
262
|
/** How many announcements were dropped by the bounded-queue fold (disclosed at render). */
|
|
253
263
|
folded: number;
|
|
@@ -598,12 +598,15 @@ function lockedJournaledUpdate(controlDir, fileName, mutate) {
|
|
|
598
598
|
releaseSidecarLock(lock, token);
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
|
-
function assertSidecarLockOwnership(lockDir, token, what) {
|
|
601
|
+
export function assertSidecarLockOwnership(lockDir, token, what, mode = "lenient") {
|
|
602
602
|
let held;
|
|
603
603
|
try {
|
|
604
604
|
held = readFileSync(join(lockDir, "owner"), "utf8");
|
|
605
605
|
}
|
|
606
|
-
catch {
|
|
606
|
+
catch (err) {
|
|
607
|
+
if (mode === "strict" && err.code !== undefined) {
|
|
608
|
+
throw new ControlPlaneCorruptError(`${what}: sidecar lock ownership UNPROVABLE at commit (owner file ${err.code === "ENOENT" ? "absent" : `unreadable: ${err.code}`}) — a fail-closed ledger must not commit on a fence it cannot prove; update aborted`, { cause: err });
|
|
609
|
+
}
|
|
607
610
|
return;
|
|
608
611
|
}
|
|
609
612
|
if (held !== token) {
|
|
@@ -897,7 +900,7 @@ function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
|
|
|
897
900
|
const { next, result } = fn(current);
|
|
898
901
|
if (next !== undefined) {
|
|
899
902
|
const data = `${JSON.stringify(next, null, 2)}\n`;
|
|
900
|
-
assertSidecarLockOwnership(lock, token, what);
|
|
903
|
+
assertSidecarLockOwnership(lock, token, what, "strict");
|
|
901
904
|
atomicWriteFileSync(journal, data);
|
|
902
905
|
atomicWriteFileSync(file, data);
|
|
903
906
|
rmSync(journal, { force: true });
|
|
@@ -932,7 +935,10 @@ function readStrictSidecar(controlDir, fileName, what) {
|
|
|
932
935
|
try {
|
|
933
936
|
journalRaw = readFileSync(journal, "utf8");
|
|
934
937
|
}
|
|
935
|
-
catch {
|
|
938
|
+
catch (err) {
|
|
939
|
+
if (err.code !== "ENOENT") {
|
|
940
|
+
throw new ControlPlaneCorruptError(`control-plane journal unreadable ((${err.code ?? "io error"})) — a committed next state may exist that cannot be proven; repair the read fault rather than serving the pre-transaction state: ${journal}`, { cause: err });
|
|
941
|
+
}
|
|
936
942
|
journalRaw = undefined;
|
|
937
943
|
}
|
|
938
944
|
if (journalRaw !== undefined) {
|
|
@@ -1421,7 +1427,7 @@ export function rebuildStrictControlPlaneLedger(controlDir, ledger, now) {
|
|
|
1421
1427
|
}
|
|
1422
1428
|
const at = now();
|
|
1423
1429
|
const quarantinedTo = [];
|
|
1424
|
-
assertSidecarLockOwnership(lock, token, what);
|
|
1430
|
+
assertSidecarLockOwnership(lock, token, what, "strict");
|
|
1425
1431
|
for (const path of [file, journal]) {
|
|
1426
1432
|
if (!existsSync(path))
|
|
1427
1433
|
continue;
|
|
@@ -1439,7 +1445,7 @@ export function rebuildStrictControlPlaneLedger(controlDir, ledger, now) {
|
|
|
1439
1445
|
}
|
|
1440
1446
|
quarantinedTo.push(dest);
|
|
1441
1447
|
}
|
|
1442
|
-
assertSidecarLockOwnership(lock, token, what);
|
|
1448
|
+
assertSidecarLockOwnership(lock, token, what, "strict");
|
|
1443
1449
|
atomicWriteFileSync(file, `${JSON.stringify(empty, null, 2)}\n`);
|
|
1444
1450
|
rmSync(journal, { force: true });
|
|
1445
1451
|
return { ledger, quarantinedTo, at };
|
|
@@ -87,6 +87,26 @@ export function prepareConfigDoors(input) {
|
|
|
87
87
|
const { deps, sessions, resume, internals } = input;
|
|
88
88
|
let spec = input.spec;
|
|
89
89
|
assertRestoreGatedToolsValue(spec.restoreGatedTools);
|
|
90
|
+
const assertToolNameListValue = (value, seat) => {
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return;
|
|
93
|
+
if (!Array.isArray(value)) {
|
|
94
|
+
const e = new Error(`TaskSpec.${seat} must be an array of tool names or absent (got ${value === null ? "null" : typeof value}) — a garbage list must refuse loudly, never be read as "no ${seat}".`);
|
|
95
|
+
e.code = "config.tool_face_control";
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
const entries = value;
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (typeof entry !== "string") {
|
|
101
|
+
const e = new Error(`TaskSpec.${seat} entries must be strings (got ${entry === null ? "null" : typeof entry}).`);
|
|
102
|
+
e.code = "config.tool_face_control";
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
assertToolNameListValue(spec.excludeTools, "excludeTools");
|
|
108
|
+
assertToolNameListValue(spec.deferTools, "deferTools");
|
|
109
|
+
assertToolNameListValue(spec.alwaysLoadTools, "alwaysLoadTools");
|
|
90
110
|
const toolFaceSnapshot = {
|
|
91
111
|
exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined,
|
|
92
112
|
defer: spec.deferTools ? Object.freeze([...spec.deferTools]) : undefined,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
2
2
|
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
|
-
import { deliverDelegationLifecycle } from "../types.js";
|
|
3
|
+
import { deliverDelegationLifecycle, deliverEngineNotice } from "../types.js";
|
|
4
4
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
5
5
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
6
6
|
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
@@ -2068,6 +2068,14 @@ export class Runner {
|
|
|
2068
2068
|
for (const p of payloads)
|
|
2069
2069
|
this.pendingSessionNotifications.pend(notificationSessionId, p);
|
|
2070
2070
|
};
|
|
2071
|
+
prepared.harness.onUndrainedUserInputs = (counts) => {
|
|
2072
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
2073
|
+
code: "task.user_steer_undrained",
|
|
2074
|
+
message: `${counts.steer + counts.followUp} user input(s) accepted as "queued" were never consumed — the run ended first ` +
|
|
2075
|
+
`(${counts.steer} steer, ${counts.followUp} follow-up). They are NOT redelivered; re-send against a live run if still wanted.`,
|
|
2076
|
+
detail: { steer: counts.steer, followUp: counts.followUp, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}) },
|
|
2077
|
+
});
|
|
2078
|
+
};
|
|
2071
2079
|
prepared.harness.onEngineNoteConsumed = (p) => {
|
|
2072
2080
|
const peer = p?.peer;
|
|
2073
2081
|
if (peer !== undefined && Array.isArray(peer.hopChain))
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4669,6 +4669,13 @@ export interface EngineNotice {
|
|
|
4669
4669
|
* value IS in force the prepare refuses with the same code as `TaskResult.errorCode` (one
|
|
4670
4670
|
* fact, one code, two loudness dialects); `detail: { raw }`.
|
|
4671
4671
|
*
|
|
4672
|
+
* - `"task.user_steer_undrained"` (#259) — user steers/follow-ups whose receipts said "queued"
|
|
4673
|
+
* were still in the queues at agent_end: the run ended before any turn could drain them. They
|
|
4674
|
+
* are NOT redelivered (a steer aimed at a finished run must not fire at the next one — unlike
|
|
4675
|
+
* ENGINE notes, which pend per session); the notice is the loud half of the #257 contract's
|
|
4676
|
+
* "accepted = enqueued, not consumed" sentence; `detail: { steer, followUp, taskId? }`.
|
|
4677
|
+
* Per-run, at most once (the terminal sweep is a single site).
|
|
4678
|
+
*
|
|
4672
4679
|
* Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
|
|
4673
4680
|
* transient network failure being retried). Those are per-attempt liveness frames with their own
|
|
4674
4681
|
* frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
|
|
@@ -80,6 +80,16 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
80
80
|
private thinkingLevel;
|
|
81
81
|
/** RB-30 terminal fix — runner-set sink for engine-note payloads left undrained at agent_end. */
|
|
82
82
|
onUndrainedEngineNotes?: (payloads: unknown[]) => void;
|
|
83
|
+
/** backlog #259 — USER-authored queue remnants at agent_end: steers/follow-ups whose receipts said
|
|
84
|
+
* "queued" but that no turn will ever drain (the run ended first). The engine-note sweep above
|
|
85
|
+
* hands ENGINE payloads back for redelivery; user inputs have no redelivery semantics (a steer
|
|
86
|
+
* aimed at a finished run must not silently fire at the next one), so their loss is ANNOUNCED
|
|
87
|
+
* instead — the runner surfaces it as an operator notice, closing the "accepted then silently
|
|
88
|
+
* dropped" window the #257 contract could only document. */
|
|
89
|
+
onUndrainedUserInputs?: (counts: {
|
|
90
|
+
steer: number;
|
|
91
|
+
followUp: number;
|
|
92
|
+
}) => void;
|
|
83
93
|
/** design/176 — runner-set sink fired at the CONSUMPTION boundary, once per engine-note payload,
|
|
84
94
|
* in consumption order (steer/followUp drain and the turn-open nextTurn splice — the two points
|
|
85
95
|
* where a queued frame actually enters the model's input). The runner uses it to record the
|
|
@@ -99,6 +109,13 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
99
109
|
* so a double call is a no-op. */
|
|
100
110
|
recoverUndrainedEngineNotes(): void;
|
|
101
111
|
private sweepUndrainedEngineNotes;
|
|
112
|
+
/** backlog #259 — what remains in the steer/follow-up queues AFTER the engine-note sweep is USER
|
|
113
|
+
* input that was accepted ("queued") and will never be consumed: the run reached agent_end first.
|
|
114
|
+
* User inputs have no redelivery semantics (unlike engine notes — a steer aimed at a finished run
|
|
115
|
+
* must not fire at the next one), so the loss is ANNOUNCED, never silent. The window is a narrow
|
|
116
|
+
* race (an injection landing after the loop's final queue check), which is exactly why it needs a
|
|
117
|
+
* loud terminal account rather than an e2e reproduction. Returns the counts for the settled frame. */
|
|
118
|
+
private announceUndrainedUserInputs;
|
|
102
119
|
private systemPrompt;
|
|
103
120
|
/** S4: physical system blocks (static per leg, additive — see AgentHarnessOptions.systemBlocks). */
|
|
104
121
|
private systemBlocks;
|
|
@@ -158,6 +158,7 @@ export class AgentHarness {
|
|
|
158
158
|
model;
|
|
159
159
|
thinkingLevel;
|
|
160
160
|
onUndrainedEngineNotes;
|
|
161
|
+
onUndrainedUserInputs;
|
|
161
162
|
onEngineNoteConsumed;
|
|
162
163
|
recoverUndrainedEngineNotes() {
|
|
163
164
|
this.sweepUndrainedEngineNotes([this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
|
|
@@ -182,6 +183,17 @@ export class AgentHarness {
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
}
|
|
186
|
+
announceUndrainedUserInputs() {
|
|
187
|
+
const counts = { steer: this.steerQueue.length, followUp: this.followUpQueue.length };
|
|
188
|
+
if ((counts.steer > 0 || counts.followUp > 0) && this.onUndrainedUserInputs) {
|
|
189
|
+
try {
|
|
190
|
+
this.onUndrainedUserInputs(counts);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return counts;
|
|
196
|
+
}
|
|
185
197
|
systemPrompt;
|
|
186
198
|
systemBlocks;
|
|
187
199
|
streamOptions;
|
|
@@ -620,9 +632,15 @@ export class AgentHarness {
|
|
|
620
632
|
if (event.type === "agent_end") {
|
|
621
633
|
await this.flushPendingSessionWrites();
|
|
622
634
|
this.sweepUndrainedEngineNotes([this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
|
|
635
|
+
const undrainedUser = this.announceUndrainedUserInputs();
|
|
623
636
|
this.phase = "idle";
|
|
624
637
|
await this.emitAny(event, signal);
|
|
625
|
-
await this.emitOwn({
|
|
638
|
+
await this.emitOwn({
|
|
639
|
+
type: "settled",
|
|
640
|
+
nextTurnCount: this.nextTurnQueue.length,
|
|
641
|
+
...(undrainedUser.steer > 0 ? { undrainedSteerCount: undrainedUser.steer } : {}),
|
|
642
|
+
...(undrainedUser.followUp > 0 ? { undrainedFollowUpCount: undrainedUser.followUp } : {}),
|
|
643
|
+
}, signal);
|
|
626
644
|
return;
|
|
627
645
|
}
|
|
628
646
|
await this.emitAny(event, signal);
|
|
@@ -815,6 +815,11 @@ export interface AbortEvent {
|
|
|
815
815
|
export interface SettledEvent {
|
|
816
816
|
type: "settled";
|
|
817
817
|
nextTurnCount: number;
|
|
818
|
+
/** backlog #259 (additive) — USER steers accepted ("queued") but never drained before agent_end.
|
|
819
|
+
* Present only when > 0; their loss is announced through `onUndrainedUserInputs` too. */
|
|
820
|
+
undrainedSteerCount?: number;
|
|
821
|
+
/** backlog #259 (additive) — same for the follow-up queue. */
|
|
822
|
+
undrainedFollowUpCount?: number;
|
|
818
823
|
}
|
|
819
824
|
export interface BeforeAgentStartEvent<TSkill extends Skill = Skill, TPromptTemplate extends PromptTemplate = PromptTemplate> {
|
|
820
825
|
type: "before_agent_start";
|