@sema-agent/core 7.32.0 → 7.32.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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.32.1 — 2026-09-25
4
+
5
+ ### Fix — a park that does not pause the env no longer stops the session's background shells (#1120; behavior; +0 exports, +0 wire keys) (@cli @server)
6
+
7
+ - **Before.** A durable park on a park-only target (a remote env with `capabilities.suspendable: false`, whose workspace persists on the target: a tool ask under `durableApproval`, a plan-review park, a resource-slice park) swept the env keeping only rows already retained. The run's teardown keeps every registered shell row still running on the env. One question, two rules: a session's `run_in_background` loop outlived every turn, yet was stopped the moment the run parked — for example while a plan-approval card was on screen — and its row read `running` until the next watcher poll, then `failed` (measured: the process was already gone at the checkpoint commit). A parked run also stopped the non-retained loops of other sessions sharing the same env.
8
+ - **Now.** One function answers both boundaries: a declared retain lane (the env's or the task's) sweeps nothing; otherwise every registered shell row still running on the env is spared, any owner, and unregistered env shells are disposed. The run's teardown that follows the park still collects the run's own rows that end with it (a session-less direct mount's rows: stop source `parent` where the park's sweep used to mark `env_sweep`). A park that pauses the env (`suspendVM`) still disposes every background process first. The teardown's gate now reads the env's retain declaration too: for an env that honours its own declaration nothing changes (its dispose was already a no-op); an adapter that declares retain on its capability face but disposes anyway now keeps its unregistered shells at a run's end as well.
9
+ - **Who needs it.** A host that declared `TaskSpec.retainBackgroundProcesses` on every turn only to keep a session's loop across an approval card, and any session waiting on a park-only approval.
10
+ - **Who is hurt / compensation.** A deployment that relied on a park to stop a session's non-retained shells — none expected. The session terminal (`reapSessionBackground`) and the exit hard face (`killAllBackgroundShellsNow`) end them as before; a teardown on an isolated env destroyed with the run still spares nothing.
11
+ - **Known limit.** A park-only park resumed in ANOTHER process: the session's loop still runs on the target, but the resuming process does not track it, and the resume's orphan notice lists it as stopped (KL-1120a, #1126; retained rows had this before).
12
+ - Pins: `test/backlog1120-park-sweep-rule.test.ts` (three park lanes × task lane absent / declared on real processes, an unregistered shell as the negative control, a second session sharing the env, a failed park commit, a persistent Monitor, a same-process resume, the parking run's own run-scoped row, the env lane read by the engine's gate, five teardown controls). Contract: `docs/background-process-lifetime.md`. Wire sheet: `docs/wire-consumption/unreleased-1120.md`.
13
+
3
14
  ## 7.32.0 — 2026-09-25
4
15
 
5
16
  ### The ask names WHICH mandate it stands on — `AskRequest.mandate` + `PendingAction.tool_approval.mandate` + `CheckpointSummary.mandate` (#1092; additive; +3 exports `PERSISTED_RULE_MANDATES` / `PersistedRuleMandate` / `isPersistedRuleMandate`; +4 optional type keys (`AskRequest`, the `PermissionResult` ask arm, the park row, the summary); no checkpoint-version bump) (@cli @client-core @server)
@@ -153,9 +153,10 @@ export interface BackgroundRetainLanes {
153
153
  }
154
154
  /**
155
155
  * THE retain predicate — "is this background process kept alive past the automatic reaps?" Every face that
156
- * answers it calls this function (Bash description + receipts, Monitor receipt, the row stamp, teardown settle
157
- * and dispose, park-only sweeps, session reap, the exit hard face, the listing). Any declared lane retains,
158
- * unless the owning isolated env is being destroyed. Contract: docs/background-process-lifetime.md.
156
+ * answers it calls this function (Bash description + receipts, Monitor receipt, the row stamp, teardown settle,
157
+ * the run-boundary dispose at teardown and park-only parks, session reap, the exit hard face, the listing).
158
+ * Any declared lane retains, unless the owning isolated env is being destroyed.
159
+ * Contract: docs/background-process-lifetime.md.
159
160
  */
160
161
  export declare function retainsBackgroundProcesses(lanes: BackgroundRetainLanes): boolean;
161
162
  /** The env lane of {@link BackgroundRetainLanes}, read off an env (false for an env without the capability). */
@@ -192,12 +193,27 @@ export declare function sweepBackgroundShells(env: ExecutionEnv & BackgroundShel
192
193
  except?: readonly string[];
193
194
  }): Promise<void>;
194
195
  /**
195
- * The park-only sweep (a durable park that does not pause the env), one entry point for every park lane:
196
- * nothing is swept when this run's task or the env declares retain; otherwise every row already retained on
197
- * this env (an earlier run's delivered service) is spared.
196
+ * THE env-wide dispose at a run boundary: the run's teardown (every exit path) and the park-only park (a
197
+ * durable park that does not pause the env — its workspace, and the processes on it, persist on the target).
198
+ * One rule for "which background shells does this boundary take":
199
+ *
200
+ * - nothing, when THE retain predicate holds for the lanes the boundary sees (the env's declaration, the
201
+ * run's `TaskSpec` lane; `envDying` overrides both);
202
+ * - otherwise every registered shell row still running on the env is spared (`runningShellIds`, any owner):
203
+ * what the run's own settle kept (session-resident rows, in-window Monitors, retained rows) and every other
204
+ * run's or session's rows sharing the env. Unregistered env shells are disposed;
205
+ * - `envDying` (an isolated owned env destroyed with this run) spares nothing: its processes die with it.
206
+ *
207
+ * A park and a teardown ask the same question because a park-only park IS the run ending for now: the run's
208
+ * teardown follows every park, its settle collects the run's own rows that end with it, and its dispose
209
+ * spares the rest — a park that took more would take what the run's end keeps (a session's loop, killed
210
+ * while its approval card is on screen). The sweep before a snapshot pause is a different question (the VM
211
+ * pause must not capture detached jobs; resume would revive untracked zombies), which is why those sites
212
+ * call {@link sweepBackgroundShells} with no keep-list.
198
213
  */
199
- export declare function sweepParkOnlyBackgroundShells(env: ExecutionEnv & BackgroundShellCapability, registry: EnvSweepAttribution & {
200
- retainedShellIds(env: unknown): readonly string[];
214
+ export declare function sweepRunBoundaryBackgroundShells(env: ExecutionEnv & BackgroundShellCapability, registry: EnvSweepAttribution & {
215
+ runningShellIds(env: unknown): readonly string[];
201
216
  }, lanes: {
202
217
  task: boolean;
218
+ envDying?: boolean;
203
219
  }): Promise<void>;
@@ -33,9 +33,10 @@ export async function sweepBackgroundShells(env, attribution, opts) {
33
33
  }
34
34
  await env.disposeBackgroundShells(opts?.except !== undefined ? { except: opts.except } : undefined);
35
35
  }
36
- export async function sweepParkOnlyBackgroundShells(env, registry, lanes) {
37
- if (retainsBackgroundProcesses({ env: envDeclaresRetain(env), task: lanes.task }))
36
+ export async function sweepRunBoundaryBackgroundShells(env, registry, lanes) {
37
+ const envDying = lanes.envDying === true;
38
+ if (retainsBackgroundProcesses({ env: envDeclaresRetain(env), task: lanes.task, envDying }))
38
39
  return;
39
- const except = registry.retainedShellIds(env);
40
+ const except = envDying ? [] : registry.runningShellIds(env);
40
41
  await sweepBackgroundShells(env, registry, except.length > 0 ? { except } : undefined);
41
42
  }
@@ -1,4 +1,4 @@
1
- import { hasBackgroundShell, sweepBackgroundShells, sweepParkOnlyBackgroundShells } from "../background-shell.js";
1
+ import { hasBackgroundShell, sweepBackgroundShells, sweepRunBoundaryBackgroundShells } from "../background-shell.js";
2
2
  import { checkpointVersionFor, debitLedger, mintCheckpointId, mintCheckpointToken, } from "../checkpoint-store.js";
3
3
  import { defaultTaskRegistry } from "../task-registry.js";
4
4
  import { checkpointScopeOf } from "./checkpoint-scope.js";
@@ -37,7 +37,7 @@ export function prepareBoundaryParks(input) {
37
37
  }
38
38
  else if (parkOnlyRemoteEnv !== undefined) {
39
39
  if (hasBackgroundShell(parkOnlyRemoteEnv))
40
- await sweepParkOnlyBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
40
+ await sweepRunBoundaryBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
41
41
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
42
42
  }
43
43
  const token = mintCheckpointToken();
@@ -120,7 +120,7 @@ export function prepareBoundaryParks(input) {
120
120
  }
121
121
  else if (parkOnlyRemoteEnv !== undefined) {
122
122
  if (hasBackgroundShell(parkOnlyRemoteEnv))
123
- await sweepParkOnlyBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
123
+ await sweepRunBoundaryBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
124
124
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
125
125
  }
126
126
  const token = mintCheckpointToken();
@@ -1,7 +1,7 @@
1
1
  import { uuidv7 } from "../../engine/session/uuid.js";
2
2
  import { Value } from "typebox/value";
3
3
  import { ASK_USER_QUESTION_TOOL_NAME, classifyQuestionOutcome, validateAskQuestions } from "../ask-question.js";
4
- import { hasBackgroundShell, sweepBackgroundShells, sweepParkOnlyBackgroundShells } from "../background-shell.js";
4
+ import { hasBackgroundShell, sweepBackgroundShells, sweepRunBoundaryBackgroundShells } from "../background-shell.js";
5
5
  import { boundInputHashOf } from "../canonical-json.js";
6
6
  import { buildRiskDescriptor, checkpointVersionFor, debitLedger, encodeAtFidelity, mintCheckpointId, mintCheckpointToken, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
7
7
  import { pauseOf } from "../pause-registry.js";
@@ -230,7 +230,7 @@ export function prepareParkAsk(input) {
230
230
  }
231
231
  else if (parkOnlyRemoteEnv !== undefined) {
232
232
  if (hasBackgroundShell(parkOnlyRemoteEnv))
233
- await sweepParkOnlyBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
233
+ await sweepRunBoundaryBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
234
234
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
235
235
  }
236
236
  token = mintCheckpointToken();
@@ -2,7 +2,7 @@ import { MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
2
2
  import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
3
3
  import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
4
4
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
5
- import { hasBackgroundShell, retainsBackgroundProcesses, sweepBackgroundShells } from "../background-shell.js";
5
+ import { hasBackgroundShell, sweepRunBoundaryBackgroundShells } from "../background-shell.js";
6
6
  import { resolveCheckpointStore } from "../checkpoint-store.js";
7
7
  import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
8
8
  import { modelCostToPricing } from "../pricing.js";
@@ -315,11 +315,9 @@ export async function runSettleAndTeardown(input) {
315
315
  retainProcesses: spec.retainBackgroundProcesses === true,
316
316
  }), "taskRegistry.settleKilledForOwner", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
317
317
  const bgEnv = prepared.ownedEnv ?? runner.deps.executionEnv;
318
- const retainBgBySpec = retainsBackgroundProcesses({ task: spec.retainBackgroundProcesses === true, envDying: ownedEnvDying });
319
- if (bgEnv && hasBackgroundShell(bgEnv) && !retainBgBySpec) {
318
+ if (bgEnv && hasBackgroundShell(bgEnv)) {
320
319
  try {
321
- const keepAlive = ownedEnvDying && bgEnv === prepared.ownedEnv ? [] : defaultTaskRegistry.runningShellIds(bgEnv);
322
- await settleTeardownLeg(() => sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined), "sweepBackgroundShells", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
320
+ await settleTeardownLeg(() => sweepRunBoundaryBackgroundShells(bgEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true, envDying: ownedEnvDying }), "sweepBackgroundShells", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
323
321
  }
324
322
  catch (e) {
325
323
  runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
@@ -140,7 +140,7 @@ export interface BackgroundBashTaskHandle extends SemaTaskHandle {
140
140
  * field reads as not marked. The row is the delivered daemon's ONLY management surface — while the process runs it survives the
141
141
  * per-run evict (TaskOutput/TaskStop resolve through this map; the watcher must stay live to mint
142
142
  * the natural-exit receipt) and rides every later run's dispose except-list on the same env
143
- * ({@link TaskRegistry.retainedShellIds}). A terminal row evicts normally. Terminals: explicit
143
+ * ({@link TaskRegistry.runningShellIds}). A terminal row evicts normally. Terminals: explicit
144
144
  * TaskStop, the process's own exit (watcher settle), or env destruction (watcher env-death settle)
145
145
  * — deliberately NO timed reaper (the deliverable is the living process). A retained SESSION-resident
146
146
  * row is likewise exempt from the session-release reap ({@link TaskRegistry.reapSessionBackground}):
@@ -610,10 +610,13 @@ export declare class TaskRegistry {
610
610
  * completion notification at the terminal state. `unref()`d — a watcher never keeps the process alive.
611
611
  * Stops itself on terminal, on a vanished shell (env disposed), or when the handle is evicted. */
612
612
  private startBashWatcher;
613
- /** RETAINED shells (THE retain predicate) still running on THIS env, any owner — the park-only sweep's
614
- * spare list. Env identity is part of the match (a shared registry never exempts another env's ids). */
613
+ /** RETAINED shells (THE retain predicate) still running on THIS env, any owner. Env identity is part of the
614
+ * match (a shared registry never exempts another env's ids). No caller in src since the park-only parks
615
+ * moved to the run-boundary dispose (which spares every running row: {@link runningShellIds}); removal
616
+ * scheduled for the next minor. */
615
617
  retainedShellIds(env: unknown): BackgroundShellId[];
616
- /** Every shell-lane row still RUNNING on this env, any owner — the run teardown's dispose except-list.
618
+ /** Every shell-lane row still RUNNING on this env, any owner — the run-boundary dispose's except-list (the
619
+ * run teardown and the park-only parks: `sweepRunBoundaryBackgroundShells`).
617
620
  * A run's teardown disposes only shells it can attribute to itself: its settle sweep has already killed
618
621
  * and settled the rows of this run that end with it, so whatever registered row is still running here is
619
622
  * either something that settle kept (session-resident, an in-window Monitor, retained) or belongs to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.32.0",
3
+ "version": "7.32.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -0,0 +1,78 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.32.1",
4
+ "from": "5aeef6b3eb1ec7dfc7aef1dd0c9f6a7aebfe3cfd",
5
+ "to": "v7.32.1",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [],
13
+ "typeKeysUnreadableArms": [],
14
+ "typeKeysFlattenedCount": 217,
15
+ "declarationMoves": [],
16
+ "mintMoves": [],
17
+ "criteriaObserves": [
18
+ {
19
+ "kind": "taskEventField",
20
+ "name": "tool_end.output",
21
+ "assertions": [
22
+ "G1.a"
23
+ ]
24
+ },
25
+ {
26
+ "kind": "typeMember",
27
+ "name": "BackgroundPoll.status",
28
+ "assertions": [
29
+ "G1.a"
30
+ ]
31
+ },
32
+ {
33
+ "kind": "typeMember",
34
+ "name": "BackgroundShellHardKillReceipt.outcome",
35
+ "assertions": [
36
+ "G1.b"
37
+ ]
38
+ },
39
+ {
40
+ "kind": "typeMember",
41
+ "name": "TaskRegistry.list",
42
+ "assertions": [
43
+ "G1.a",
44
+ "G1.b"
45
+ ]
46
+ },
47
+ {
48
+ "kind": "typeMember",
49
+ "name": "TaskRegistry.reapSessionBackground",
50
+ "assertions": [
51
+ "G1.b"
52
+ ]
53
+ },
54
+ {
55
+ "kind": "typeMember",
56
+ "name": "TaskRegistry.runningShellIds",
57
+ "assertions": [
58
+ "G1.a"
59
+ ]
60
+ },
61
+ {
62
+ "kind": "typeMember",
63
+ "name": "TaskSpec.retainBackgroundProcesses",
64
+ "assertions": [
65
+ "G1.a"
66
+ ]
67
+ }
68
+ ],
69
+ "consumersHint": {
70
+ "cli": [],
71
+ "server": [],
72
+ "sdk": [],
73
+ "note": "scanned cli@4e593722 server@582dee2f sdk@9b5e65b — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
74
+ },
75
+ "breakingCount": 0,
76
+ "breakingItems": [],
77
+ "toResolvedAt": "b09a04bd1f9e09fd7e28914d488f12f517dc8d4c"
78
+ }