@sema-agent/core 7.35.0 → 7.35.1
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 +33 -0
- package/dist/core/runner/contracts.d.ts +1 -1
- package/dist/core/runner/prepare-task.d.ts +3 -1
- package/dist/core/runner/prepare-task.js +2 -1
- package/dist/core/runner/run-notification-lane.d.ts +2 -0
- package/dist/core/runner/run-notification-lane.js +2 -2
- package/dist/core/runner/runtask.d.ts +2 -1
- package/dist/core/runner/runtask.js +15 -5
- package/package.json +1 -1
- package/wire-changes/core@7.35.1.json +59 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## 7.35.1 — 2026-09-29
|
|
2
|
+
|
|
3
|
+
### 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)
|
|
4
|
+
|
|
5
|
+
- **Before (v7.35.0).** A run that named its session (`TaskSpec.sessionId`) took the Runner's session lock before its prepare; a run that named none took no lock. Its session store minted the id inside prepare, and the id reached other callers through the run's tool contexts and events, so a second run started on it ran beside the first. Run A (`bypassPermissions`, a new session) spawned a child and was still running, or was cancelled with its tool pending; run B (`plan`) started on A's session id reached its first tool at once, and the child's ceiling kept answering A's `bypassPermissions` while B ran (KL-1192-c). Not a regression: v7.34.0's child held A's controller for life.
|
|
6
|
+
- **Now (one lock rule).** Every run holds ONE session lock from the moment it knows the session id to the run's end, decided by one read of the spec. A session the spec names is locked before prepare (unchanged). For a spec that names none, prepare takes the lock the moment the store mints the id. It is the same lock, released at the same place. Asking a run's hold again for the same id returns the same hold; asking it for another id is refused by name (`internal.session_lock_second_id`, an engine invariant no engine path reaches) rather than taking a second lock over the first. A second run on a minted id queues behind the first exactly as it does behind a named session; once the first run stops, the second enters, and a child of the first then follows it (`plan` refuses the child's write). Runs on different sessions are not serialized. The child ceiling rule (#1192) is unchanged.
|
|
7
|
+
- **Timing.** Measured on a prepare refusal: a run that named no session settles four microtasks later (46 → 50: the lock's await on the prepare path); a run naming its session reaches prepare two microtasks later (56 → 58: the hold's own step before the lock).
|
|
8
|
+
- **Who is hurt.** A caller that starts a second run on a new session's id while the first still runs sees that run start after the first ends: the order changes, not the outcome. The exception is a caller tool that starts a run on its OWN run's session id and awaits it: that now waits forever, as it already did for a run that named its session (no engine path does this). A probe that pinned the old overlap (two runs of one session side by side because the first named no session) goes red; rewrite it to expect the second run to queue, as the re-pinned cells below do. The lock is per Runner instance, so two Runners over one session store are not serialized, as for a named session. A caller that reads the freshly minted id off the store (`TtlSessionStore.list()`) and starts a run on it before the first run's prepare took the lock runs first; the first then queues behind it — the order is reversed and nothing waits forever (KL-1206).
|
|
9
|
+
- Pins: `test/backlog1206-new-session-lock.test.ts` ①–⑧, on a real Runner:
|
|
10
|
+
- ① / ② the second run queues behind a new-session run that was cancelled with its tool pending, or is still running. The reading is the named-session one, and the child then follows the second run's `plan`.
|
|
11
|
+
- ③ / ④ controls.
|
|
12
|
+
- ⑤ the lock is released on a prepare throw after the mint and on an abort.
|
|
13
|
+
- ⑥ tripwire for the retired shape: the second run's prepare does not begin while the first run runs, however the first got its session.
|
|
14
|
+
- ⑦ / ⑧ one decision per run: a `sessionId` inherited through the prototype chain, which the plan-mode rebind drops, with the run refused after prepare acquired, releases the named session's lock (the next run on it enters); a caller clearing its `sessionId` while the store's acquire is pending does not wait on its own lock and runs on the session it acquired. v7.35.0 green; the first cut of this fix red (it held twice and overwrote the first release, or waited on its own lock).
|
|
15
|
+
|
|
16
|
+
`test/backlog1206-session-lock-one-hold.test.ts` ⑨ / ⑩ (the lane's hold verb reached where prepare receives it): the same id again is the same hold, another id is refused by `internal.session_lock_second_id` and the first lock is still released at the run's end; a spec naming its session hands prepare no hold verb.
|
|
17
|
+
|
|
18
|
+
Re-pinned where the old overlap was the setup:
|
|
19
|
+
- `backlog1192-session-mode-seat` C1 / C3: the second run queues, and the child keeps the running leg's `plan`; `wrote` is 0 as before.
|
|
20
|
+
- `backlog744-notification-session-routing`, the two overlapping-run cells: the second run waits, the frame the idle first run cannot take parks, and a same-key retry folds on the producer's lane. The waiting run's turn-open carries it once.
|
|
21
|
+
- `design393-s6-run-leg-lanes`: rejection depth 46 → 50, because the lock's await is on the prepare path.
|
|
22
|
+
|
|
23
|
+
Mutations:
|
|
24
|
+
- D950, a new session is not locked: ①, ②, ⑥, C1, C3 and the rejection depth go red.
|
|
25
|
+
- D951, the minted lock is never released: ①, ②, ③, ⑤, ⑥, ⑨ go red.
|
|
26
|
+
- D952, the lock is released only when the run body resolves: ⑤ and ⑦ go red.
|
|
27
|
+
- D953, every minted session locks one shared key: control ④ goes red.
|
|
28
|
+
- D955, the hold keeps no owner: ⑨ goes red.
|
|
29
|
+
- D956, prepare is handed the hold for a named session too: ⑦ and ⑩ go red.
|
|
30
|
+
- The first cut of this fix (a second decision in prepare): ⑦, ⑧, ⑨ and ⑩ go red.
|
|
31
|
+
|
|
32
|
+
Known limits: KL-1192-c closed; KL-1206 (the mint-to-lock window).
|
|
33
|
+
|
|
1
34
|
## 7.35.0 — 2026-09-29
|
|
2
35
|
|
|
3
36
|
**Hard-breaking changes — the whole release in three sentences.**
|
|
@@ -2771,7 +2771,7 @@ export interface RunnerSelfSeat {
|
|
|
2771
2771
|
*/
|
|
2772
2772
|
export type PrepareTaskFn = (spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals, runnerSelf?: RunnerSelfSeat, runIdSink?: {
|
|
2773
2773
|
runId?: string;
|
|
2774
|
-
}) => Promise<Prepared>;
|
|
2774
|
+
}, holdSessionLock?: (sessionId: string) => Promise<void>) => Promise<Prepared>;
|
|
2775
2775
|
/**
|
|
2776
2776
|
* The Result of an INSTALLING phase: a lane whose every product is installed on a seat it borrowed
|
|
2777
2777
|
* (the harness's stop gate and recovery chain, the run state's counter groups, the stream's result setter, the claimed
|
|
@@ -113,7 +113,9 @@ runnerSelf?: RunnerSelfSeat,
|
|
|
113
113
|
* shape the Runner uses for the generated taskId/sessionId. */
|
|
114
114
|
runIdSink?: {
|
|
115
115
|
runId?: string;
|
|
116
|
-
}
|
|
116
|
+
},
|
|
117
|
+
/** The Runner's lock on this leg's session, handed only when the spec named none: taken the moment the store mints the id. */
|
|
118
|
+
holdSessionLock?: (sessionId: string) => Promise<void>): Promise<Prepared>;
|
|
117
119
|
/** The entry IS the contract the notification lane's seat names ({@link PrepareTaskFn}, contracts.ts):
|
|
118
120
|
* a signature change reds HERE, at the declaration, as `false` failing the `true` bound. IDENTITY, not assignability:
|
|
119
121
|
* the parameter tuple (arity, optionality, each type) and the return type must be the SAME type in both directions —
|
|
@@ -282,7 +282,7 @@ function refusePermissionModeDoors(spec) {
|
|
|
282
282
|
}
|
|
283
283
|
return mode;
|
|
284
284
|
}
|
|
285
|
-
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf, runIdSink) {
|
|
285
|
+
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf, runIdSink, holdSessionLock) {
|
|
286
286
|
const runId = uuidv7();
|
|
287
287
|
if (runIdSink !== undefined)
|
|
288
288
|
runIdSink.runId = runId;
|
|
@@ -308,6 +308,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
308
308
|
const fileHistoryRequest = resolveFileHistoryRequest(spec, deps);
|
|
309
309
|
const { acquired, session, conflictRef, wakeRecovered } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(internals?.sessionPlacement !== undefined ? { placement: internals.sessionPlacement } : {}), ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
|
|
310
310
|
const sessionId = acquired.sessionId;
|
|
311
|
+
await holdSessionLock?.(sessionId);
|
|
311
312
|
const onceLedger = await loadAnnounceOnceLedger(session, resume?.seed.announcedListings, deps.onError);
|
|
312
313
|
const hostTaskId = spec.taskId ?? sessionId;
|
|
313
314
|
const delegation = effectiveDelegationFacts(internals, resume?.seed.isDelegatedChild);
|
|
@@ -17,6 +17,8 @@ export interface RunNotificationLaneInput<R> {
|
|
|
17
17
|
internals: RunInternals | undefined;
|
|
18
18
|
/** borrowed-mutable — the stream layer's backstop carrier, handed to prepare so a prepare THROW still names its run. */
|
|
19
19
|
taskIdRef: TaskIdRef | undefined;
|
|
20
|
+
/** borrowed-readonly — the stream layer's session-lock verb, handed to prepare; absent when the spec named its session. */
|
|
21
|
+
holdSessionLock: ((sessionId: string) => Promise<void>) | undefined;
|
|
20
22
|
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError` in the park closure — after the run, on a swapped
|
|
21
23
|
* catalog it is still the CURRENT sink; `models` and prepare's `deps` at the natural-snapshot pin); the one home of why it
|
|
22
24
|
* is a seat and not a captured object is {@link RunnerDepsSeat}. */
|
|
@@ -8,7 +8,7 @@ export function runNotificationLane(input) {
|
|
|
8
8
|
return obligations.run(() => runNotificationLaneAs(input, obligations));
|
|
9
9
|
}
|
|
10
10
|
async function runNotificationLaneAs(input, obligations) {
|
|
11
|
-
const { spec, queue, resume, internals, taskIdRef, runner, self, sessions, sessionReadStates, sessionModeSeats, pendingSessionNotifications, prepareTask, next } = input;
|
|
11
|
+
const { spec, queue, resume, internals, taskIdRef, holdSessionLock, runner, self, sessions, sessionReadStates, sessionModeSeats, pendingSessionNotifications, prepareTask, next } = input;
|
|
12
12
|
const prepareResume = resume
|
|
13
13
|
? {
|
|
14
14
|
leafId: resume.cp.leafId,
|
|
@@ -127,7 +127,7 @@ async function runNotificationLaneAs(input, obligations) {
|
|
|
127
127
|
}
|
|
128
128
|
return injectTaskNotification(notification, opts);
|
|
129
129
|
},
|
|
130
|
-
}, self, taskIdRef);
|
|
130
|
+
}, self, taskIdRef, holdSessionLock);
|
|
131
131
|
pendingSessionNotifications.bindSessionBox(prepared.sessionId, prepared.sessionBox);
|
|
132
132
|
const notificationLane = {
|
|
133
133
|
get harness() { return notificationHarness; }, set harness(h) { notificationHarness = h; },
|
|
@@ -94,7 +94,8 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
94
94
|
toolPolicy?: RunnerDeps["toolPolicy"];
|
|
95
95
|
hooks?: RunnerDeps["hooks"];
|
|
96
96
|
};
|
|
97
|
-
/** Acquire the lock for a sessionId; returns a release fn.
|
|
97
|
+
/** Acquire the lock for a sessionId; returns a release fn. The one lock every leg holds on its session, from the moment it
|
|
98
|
+
* knows the id to the leg's end (through `holdSessionLock` in `runTaskStream`, one per leg). */
|
|
98
99
|
private acquireSessionLock;
|
|
99
100
|
/**
|
|
100
101
|
* Hot-swap the deployment seats a running Runner may change without a restart — the ONE door for every
|
|
@@ -330,14 +330,24 @@ export class Runner {
|
|
|
330
330
|
};
|
|
331
331
|
const runStartedAt = Date.now();
|
|
332
332
|
const taskIdRef = { current: spec.taskId, sessionId: spec.sessionId };
|
|
333
|
+
let held;
|
|
334
|
+
let releaseLock;
|
|
335
|
+
const holdSessionLock = (sessionId) => {
|
|
336
|
+
if (held === undefined)
|
|
337
|
+
held = { sessionId, taken: this.acquireSessionLock(sessionId).then((release) => void (releaseLock = release)) };
|
|
338
|
+
else if (held.sessionId !== sessionId)
|
|
339
|
+
return Promise.reject(Object.assign(new Error(`internal invariant: a leg holds one session lock — it holds "${held.sessionId}" and was asked for "${sessionId}"`), { code: "internal.session_lock_second_id" }));
|
|
340
|
+
return held.taken;
|
|
341
|
+
};
|
|
342
|
+
const namedSessionId = spec.sessionId;
|
|
333
343
|
const run = (async () => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
: undefined;
|
|
344
|
+
if (namedSessionId)
|
|
345
|
+
await holdSessionLock(namedSessionId);
|
|
337
346
|
try {
|
|
338
347
|
await this.runLocked({
|
|
339
348
|
spec,
|
|
340
349
|
queue,
|
|
350
|
+
holdSessionLock: namedSessionId ? undefined : holdSessionLock,
|
|
341
351
|
setResult: (r) => {
|
|
342
352
|
resultValue = r;
|
|
343
353
|
const pcs = internals?.inheritedGate?.parentConstraints;
|
|
@@ -436,9 +446,9 @@ export class Runner {
|
|
|
436
446
|
};
|
|
437
447
|
}
|
|
438
448
|
runLocked(input) {
|
|
439
|
-
const { spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, captureOptOutRef, mcpRedialRef, entryActor, entryTracer } = input;
|
|
449
|
+
const { spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, holdSessionLock, resume, internals, notifyRef, captureOptOutRef, mcpRedialRef, entryActor, entryTracer } = input;
|
|
440
450
|
return runNotificationLane({
|
|
441
|
-
spec, queue, resume, internals, taskIdRef,
|
|
451
|
+
spec, queue, resume, internals, taskIdRef, holdSessionLock,
|
|
442
452
|
runner: this.depsSeat, self: this, sessions: this.sessions, sessionReadStates: this.sessionReadStates, sessionModeSeats: this.sessionModeSeats, pendingSessionNotifications: this.pendingSessionNotifications,
|
|
443
453
|
prepareTask,
|
|
444
454
|
next: ({ prepared, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, modelCatalog, notificationLane, unsubscribeTaskNotifications }) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.35.1",
|
|
4
|
+
"from": "b0a5a6ddd0b430433d12452260aa51cf96f05972",
|
|
5
|
+
"to": "v7.35.1",
|
|
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": "SessionStore.acquire",
|
|
21
|
+
"assertions": [
|
|
22
|
+
"G1.c"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"kind": "typeMember",
|
|
27
|
+
"name": "TaskResult.sessionId",
|
|
28
|
+
"assertions": [
|
|
29
|
+
"G1.b"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"kind": "typeMember",
|
|
34
|
+
"name": "TaskSpec.permissionMode",
|
|
35
|
+
"assertions": [
|
|
36
|
+
"G1.a",
|
|
37
|
+
"G1.d"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"kind": "typeMember",
|
|
42
|
+
"name": "TaskSpec.sessionId",
|
|
43
|
+
"assertions": [
|
|
44
|
+
"G1.a",
|
|
45
|
+
"G1.c",
|
|
46
|
+
"G1.d"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"consumersHint": {
|
|
51
|
+
"cli": [],
|
|
52
|
+
"server": [],
|
|
53
|
+
"sdk": [],
|
|
54
|
+
"note": "scanned cli@6b6025f7 server@25d6b7a6 sdk@e8f6f0d — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
|
|
55
|
+
},
|
|
56
|
+
"breakingCount": 0,
|
|
57
|
+
"breakingItems": [],
|
|
58
|
+
"toResolvedAt": "27a9b09aced4d24b63a68ca78f4186c6fd5fa944"
|
|
59
|
+
}
|