@sema-agent/core 5.4.0 → 5.5.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.5.0 (2026-08-03)
4
+
5
+ _Three follow-through items: the deferred-placeholder wrong-guess round trip halves, durable cron jobs survive their creating session, and the redaction findings channel closes its last three audit gaps._
6
+
7
+ ### Fixed
8
+
9
+ - Deferred-tool placeholders answer a wrong guess with the real schema. A placeholder card carries a name and one hint line, so a model that has to guess a shape copies the local pattern of the sibling tools it can see — and a wrong guess used to cost four rounds (call → a rejection teaching a `ToolSearch` round trip → search → real call). A shape-invalid direct call now comes back as structured invalid-arguments **with the real declared parameter schema inlined**, and activates the tool on the spot — the same activation the `ToolSearch` `select:` lane performs (full schema materialized into the next request, fingerprint/boundary-announce refresh, listing ride), because both lanes run one activation closure. Wrong guess: four rounds → two. Right guess: still zero — the schema-valid direct lane is untouched, the shape gate still refuses to run the real tool body on a call it never declared, and adjudication is unchanged (policy/approval decide before execute, so a denied call activates nothing). Two consequences stated plainly: the rejection is flagged as an error, so a later leg's transcript replay does not credit that activation (the name reverts to a placeholder and the corrected call re-activates it — self-healing, never a widening); and a withdrawn name, or a declaration that cannot be serialized, keeps the previous "activate it first" rejection.
10
+ - Scheduler isolation keys fork by lifetime. A durable cron job was filed under the same key as a session-scoped one — minted `sessionId ?? principal ?? taskId ?? "default"` — so on any host with sessions a durable job belonged to the session that created it: it kept firing, but every later session got `not_found` from `CronList`/`CronDelete` and could not even learn it existed. Durable jobs are now isolated by the stable cross-session identity (`principal`, or a single `"default"` bucket where the deployment carries no principal); session-scoped jobs keep the session chain unchanged. `CronList` reads both lanes (deduplicated, and a lane whose listing fails surfaces the error instead of a half-empty list), `CronDelete` acts on the lane that actually holds the id, and `ScheduleWakeup` stays on the session lane (one session's `stop: true` never touches another's pending wakeup). The cross-tenant refusal is unchanged: another principal's durable job answers `not_found`, never an existence leak. **Deployment note**: durable rows written by earlier versions are still keyed by their originating session — a one-time re-key of existing rows belongs to the scheduler backend; the engine does no migration.
11
+
12
+ ### Added
13
+
14
+ - The redaction findings channel completes (external-audit architecture item): findings now carry `source` (a closed provenance vocabulary — `arg-summary` / `untrusted-egress` / `secret-env` — stamped by the minting rule table, with no public finding constructor to forge); `summarizeRedactions(findings)` renders the one-line disclosure a host can announce ("3 redactions: prefixed-token (high) x1, …" — pure function, never auto-injected); and the env arm joins the channel (`scrubSecretEnv(env, findings?)` reports each deleted entry as a `SecretEnvFinding {key, kind, confidence, source}` — deletion, not replacement, so it is its own shape; one summarizer renders both). Absent the optional collector every path is byte-identical to before.
15
+
3
16
  ## 5.4.0 (2026-08-03)
4
17
 
5
18
  _The external-audit dunning batch closes end-to-end (small cases + all three cleared A-group debts), and the engine grows its outer brain-call guardrail. All additive or louder; major stays locked at 5._
@@ -1,10 +1,17 @@
1
1
  export type RedactionConfidence = "high" | "medium" | "low";
2
+ export type RedactionSource = "arg-summary" | "untrusted-egress" | "secret-env";
2
3
  export interface RedactionFinding {
3
4
  kind: string;
4
5
  span: readonly [number, number];
5
6
  confidence: RedactionConfidence;
6
7
  marker: string;
8
+ source: RedactionSource;
7
9
  }
10
+ export interface SummarizableFinding {
11
+ kind: string;
12
+ confidence: RedactionConfidence;
13
+ }
14
+ export declare function summarizeRedactions(findings: readonly SummarizableFinding[]): string;
8
15
  export interface RedactionReport {
9
16
  findings: RedactionFinding[];
10
17
  preexistingMarkers?: number;
@@ -13,6 +20,7 @@ export interface RedactionPass {
13
20
  kind: string;
14
21
  confidence: RedactionConfidence;
15
22
  marker: string;
23
+ source: RedactionSource;
16
24
  re: RegExp;
17
25
  replace: string | ((match: string, ...groups: string[]) => string);
18
26
  }
@@ -1,5 +1,16 @@
1
1
  const ACTIVITY_ARG_MAX = 80;
2
2
  const SENSITIVE_KEY = /token|secret|key|password|passwd|credential|auth/i;
3
+ export function summarizeRedactions(findings) {
4
+ if (findings.length === 0)
5
+ return "no redactions";
6
+ const counts = new Map();
7
+ for (const f of findings) {
8
+ const label = `${f.kind} (${f.confidence})`;
9
+ counts.set(label, (counts.get(label) ?? 0) + 1);
10
+ }
11
+ const parts = [...counts].map(([label, n]) => `${label} x${n}`);
12
+ return `${findings.length} redaction${findings.length === 1 ? "" : "s"}: ${parts.join(", ")}`;
13
+ }
3
14
  function mapBackOnePass(edits, pos) {
4
15
  let delta = 0;
5
16
  for (const e of edits) {
@@ -37,7 +48,13 @@ export function runRedactionPasses(input, passes, report) {
37
48
  s0 = mapBackOnePass(batches[i], s0);
38
49
  e0 = mapBackOnePass(batches[i], e0);
39
50
  }
40
- report.findings.push({ kind: pass.kind, confidence: pass.confidence, span: [s0, e0], marker: pass.marker });
51
+ report.findings.push({
52
+ kind: pass.kind,
53
+ confidence: pass.confidence,
54
+ span: [s0, e0],
55
+ marker: pass.marker,
56
+ source: pass.source,
57
+ });
41
58
  }
42
59
  edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length });
43
60
  }
@@ -48,18 +65,19 @@ export function runRedactionPasses(input, passes, report) {
48
65
  return cur;
49
66
  }
50
67
  export const SECRET_PASSES = [
51
- { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
52
- { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
53
- { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
54
- { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
55
- { kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
56
- { kind: "jwt", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
57
- { kind: "private-key-block", confidence: "high", marker: "[redacted]", re: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?(?:-----END[ A-Z]*PRIVATE KEY-----|$)/g, replace: "[redacted]" },
58
- { kind: "bearer-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
68
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
69
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
70
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
71
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
72
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
73
+ { kind: "jwt", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
74
+ { kind: "private-key-block", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?(?:-----END[ A-Z]*PRIVATE KEY-----|$)/g, replace: "[redacted]" },
75
+ { kind: "bearer-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
59
76
  {
60
77
  kind: "keyword-value",
61
78
  confidence: "medium",
62
79
  marker: "[redacted]",
80
+ source: "arg-summary",
63
81
  re: /(?<![A-Za-z0-9])((?:token|secret|key|password|passwd|credential|authorization)["']?\s*[=:]\s*["']?)[^\s"';|&]{4,}/gi,
64
82
  replace: "$1[redacted]",
65
83
  },
@@ -1469,9 +1469,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1469
1469
  if (spec.interactiveTools === true || (spec.interactiveTools !== false && (onQuestion !== undefined || durableQuestionFace)))
1470
1470
  tools.push(createAskUserQuestionTool(onQuestion, { principal: spec.principal, sourceTaskId: sessionId }));
1471
1471
  if (spec.handsReadOnly !== true) {
1472
- const schedScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
1472
+ const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
1473
1473
  tools.push(...createSchedulerTools(executionEnv, {
1474
- scope: schedScope,
1474
+ scope: sessionScope,
1475
1475
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
1476
1476
  ...(sessionId !== undefined ? { sessionId } : {}),
1477
1477
  requestStopAfterTurn,
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
3
  import { defineTool, errorResult } from "../tools.js";
4
+ import { formatZodValidationError, truncateError } from "../tool-errors.js";
4
5
  export const TOOL_SEARCH_NAME = "ToolSearch";
5
6
  const DEFER_AUTO_FRACTION = 0.1;
6
7
  const CHARS_PER_TOKEN = 4;
@@ -61,12 +62,32 @@ export function buildDeferredRegistry(deferred, tools) {
61
62
  }
62
63
  return reg;
63
64
  }
65
+ function renderSchemaForModel(schema) {
66
+ let json;
67
+ try {
68
+ json = JSON.stringify(schema);
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ return json === undefined ? undefined : truncateError(json);
74
+ }
64
75
  export function createPlaceholderTool(info, direct) {
65
76
  const sn = safeName(info.name);
66
77
  const teachingRejection = () => {
67
78
  throw new Error(`Tool "${sn}" is not active yet. Call ${TOOL_SEARCH_NAME} with {"query":"select:${sn}"} ` +
68
79
  `(or a keyword \`query\`) to load its full schema, then call ${sn} with the proper arguments.`);
69
80
  };
81
+ const invalidArgumentsRejection = (target, params, schemaJson, ride) => {
82
+ const text = `Invalid arguments for \`${sn}\`: ${formatZodValidationError(target.parameters, params)} ` +
83
+ `\`${sn}\` is now active — its full parameter schema is below (and rides the next request). ` +
84
+ `Call \`${sn}\` again with arguments matching it.\nParameter schema: ${schemaJson}`;
85
+ return {
86
+ content: ride === undefined || ride === "" ? [{ type: "text", text }] : [{ type: "text", text }, { type: "text", text: ride }],
87
+ details: { invalidArguments: true },
88
+ isError: true,
89
+ };
90
+ };
70
91
  if (direct !== undefined) {
71
92
  return {
72
93
  name: info.name,
@@ -78,14 +99,20 @@ export function createPlaceholderTool(info, direct) {
78
99
  ...(direct.executionMode !== undefined ? { executionMode: direct.executionMode } : {}),
79
100
  execute: async (toolCallId, params, signal, onUpdate) => {
80
101
  const real = direct.resolveReal();
81
- if (real !== undefined && Value.Check(real.parameters, params)) {
102
+ if (real === undefined)
103
+ return teachingRejection();
104
+ if (Value.Check(real.parameters, params)) {
82
105
  const ride = await direct.activate();
83
106
  const result = await real.invoke(toolCallId, params, signal, onUpdate);
84
107
  if (ride === undefined || ride === "")
85
108
  return result;
86
109
  return { ...result, content: [...result.content, { type: "text", text: ride }] };
87
110
  }
88
- return teachingRejection();
111
+ const schemaJson = renderSchemaForModel(real.parameters);
112
+ if (schemaJson === undefined)
113
+ return teachingRejection();
114
+ const ride = await direct.activate();
115
+ return invalidArgumentsRejection(real, params, schemaJson, ride);
89
116
  },
90
117
  };
91
118
  }
@@ -1,2 +1,10 @@
1
+ import type { RedactionConfidence, RedactionSource } from "./arg-summary.js";
2
+ export type SecretEnvFindingKind = "suffix-rule" | "exact-name";
3
+ export interface SecretEnvFinding {
4
+ key: string;
5
+ kind: SecretEnvFindingKind;
6
+ confidence: RedactionConfidence;
7
+ source: RedactionSource;
8
+ }
1
9
  export declare function isSecretEnvKey(key: string): boolean;
2
- export declare function scrubSecretEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
10
+ export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
@@ -4,14 +4,31 @@ const SECRET_ENV_EXACT_NAMES = new Set([
4
4
  "PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
5
5
  "NPM_CONFIG__AUTH", "NPM_CONFIG__AUTHTOKEN",
6
6
  ]);
7
+ const SECRET_ENV_KIND_CONFIDENCE = {
8
+ "exact-name": "high",
9
+ "suffix-rule": "medium",
10
+ };
11
+ function classifySecretEnvKey(key) {
12
+ if (SECRET_ENV_RE.test(key))
13
+ return "suffix-rule";
14
+ if (SECRET_ENV_EXACT_NAMES.has(key.toUpperCase()))
15
+ return "exact-name";
16
+ return undefined;
17
+ }
7
18
  export function isSecretEnvKey(key) {
8
- return SECRET_ENV_RE.test(key) || SECRET_ENV_EXACT_NAMES.has(key.toUpperCase());
19
+ return classifySecretEnvKey(key) !== undefined;
9
20
  }
10
- export function scrubSecretEnv(env) {
21
+ export function scrubSecretEnv(env, findings) {
11
22
  const out = {};
12
23
  for (const k of Object.keys(env)) {
13
- if (!isSecretEnvKey(k))
24
+ const kind = classifySecretEnvKey(k);
25
+ if (kind === undefined) {
14
26
  out[k] = env[k];
27
+ continue;
28
+ }
29
+ if (findings !== undefined) {
30
+ findings.push({ key: k, kind, confidence: SECRET_ENV_KIND_CONFIDENCE[kind], source: "secret-env" });
31
+ }
15
32
  }
16
33
  return out;
17
34
  }
@@ -1,5 +1,6 @@
1
1
  import { type RedactionReport } from "./arg-summary.js";
2
- export type { RedactionFinding, RedactionReport, RedactionConfidence } from "./arg-summary.js";
2
+ export type { RedactionFinding, RedactionReport, RedactionConfidence, RedactionSource, SummarizableFinding, } from "./arg-summary.js";
3
+ export { summarizeRedactions } from "./arg-summary.js";
3
4
  export declare function redactSecrets(s: string, report?: RedactionReport): string;
4
5
  export declare function redactHostLeaks(s: string, report?: RedactionReport): string;
5
6
  export declare function boundedRedactedSummary(value: unknown, max: number): string;
@@ -1,6 +1,7 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
2
  import { scrubSecrets, SECRET_PASSES, runRedactionPasses } from "./arg-summary.js";
3
3
  import { sliceHeadSafe } from "./surrogate-safe-slice.js";
4
+ export { summarizeRedactions } from "./arg-summary.js";
4
5
  const URI_SCHEME = String.raw `[A-Za-z][A-Za-z0-9+.\-_]{0,63}`;
5
6
  const CREDENTIALS_IN_AUTHORITY = new RegExp(String.raw `(${URI_SCHEME}:\/\/)((?:[^/\s"'@]*@)+)`, "gi");
6
7
  const URL_OF_ANY_PROTOCOL = new RegExp(String.raw `${URI_SCHEME}:\/\/[^\s"'\`]+`, "gi");
@@ -21,18 +22,19 @@ const SECRET_TIER_PASSES = [
21
22
  kind: "url-credentials",
22
23
  confidence: "medium",
23
24
  marker: "[redacted-credentials]",
25
+ source: "untrusted-egress",
24
26
  re: CREDENTIALS_IN_AUTHORITY,
25
27
  replace: (_m, scheme, userinfo) => redactUserinfo(scheme, userinfo),
26
28
  },
27
- { kind: "signed-url-param", confidence: "high", marker: "[redacted]", re: /([?&#](?:sig|signature|sas|code)=)[^&\s"'#]+/gi, replace: "$1[redacted]" },
29
+ { kind: "signed-url-param", confidence: "high", marker: "[redacted]", source: "untrusted-egress", re: /([?&#](?:sig|signature|sas|code)=)[^&\s"'#]+/gi, replace: "$1[redacted]" },
28
30
  ];
29
31
  export function redactSecrets(s, report) {
30
32
  return runRedactionPasses(s, SECRET_TIER_PASSES, report);
31
33
  }
32
34
  const HOST_LEAK_PASSES = [
33
35
  ...SECRET_TIER_PASSES,
34
- { kind: "url", confidence: "medium", marker: "[redacted-url]", re: URL_OF_ANY_PROTOCOL, replace: "[redacted-url]" },
35
- { kind: "fs-path", confidence: "low", marker: "[redacted-path]", re: /(?:\/[A-Za-z0-9_.-]+){2,}/g, replace: "[redacted-path]" },
36
+ { kind: "url", confidence: "medium", marker: "[redacted-url]", source: "untrusted-egress", re: URL_OF_ANY_PROTOCOL, replace: "[redacted-url]" },
37
+ { kind: "fs-path", confidence: "low", marker: "[redacted-path]", source: "untrusted-egress", re: /(?:\/[A-Za-z0-9_.-]+){2,}/g, replace: "[redacted-path]" },
36
38
  ];
37
39
  export function redactHostLeaks(s, report) {
38
40
  return runRedactionPasses(s, HOST_LEAK_PASSES, report);
package/dist/index.d.ts CHANGED
@@ -57,6 +57,7 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
57
57
  export type { KillProcessTreeOptions } from "./engine/execution-env/kill-tree.js";
58
58
  export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
59
59
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
60
+ export type { SecretEnvFinding, SecretEnvFindingKind } from "./core/secret-env.js";
60
61
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
61
62
  export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode } from "./internal/harness.js";
62
63
  export type { ExecResult } from "./internal/harness.js";
@@ -172,7 +173,8 @@ export { FileBackgroundAgentStore, type FileBackgroundAgentStoreOptions } from "
172
173
  export { A2A_TASK_STATES, type A2ATaskState, type A2ATaskStateReversal, type A2ATaskStateReversalFaithful, type A2ATaskStateReversalLossy, toA2ATaskState, fromA2ATaskState, } from "./core/a2a-task-state.js";
173
174
  export { type WorkflowJournalStore, type WorkflowJournalEntry, InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
174
175
  export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedactedSummary } from "./core/untrusted-egress.js";
175
- export type { RedactionFinding, RedactionReport, RedactionConfidence } from "./core/untrusted-egress.js";
176
+ export type { RedactionFinding, RedactionReport, RedactionConfidence, RedactionSource, SummarizableFinding, } from "./core/untrusted-egress.js";
177
+ export { summarizeRedactions } from "./core/untrusted-egress.js";
176
178
  export { MemoryRosterStore, FileRosterStore, type RosterStore, type RosterEntry, type RosterAccess, type RosterGcOptions } from "./agents/roster-store.js";
177
179
  export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, type ConfigKnob, type ConfigOverrideDeclaration, type ConfigProvenance, type EffectiveConfigField, } from "./config/catalog.js";
178
180
  export { normalizeAgentName } from "./core/task-registry.js";
package/dist/index.js CHANGED
@@ -157,6 +157,7 @@ export { FileBackgroundAgentStore } from "./stores/file/background-agent-store.j
157
157
  export { A2A_TASK_STATES, toA2ATaskState, fromA2ATaskState, } from "./core/a2a-task-state.js";
158
158
  export { InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
159
159
  export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedactedSummary } from "./core/untrusted-egress.js";
160
+ export { summarizeRedactions } from "./core/untrusted-egress.js";
160
161
  export { MemoryRosterStore, FileRosterStore } from "./agents/roster-store.js";
161
162
  export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, } from "./config/catalog.js";
162
163
  export { normalizeAgentName } from "./core/task-registry.js";
@@ -27,9 +27,12 @@ The runtime clamps to [60, 3600], so you don't need to clamp yourself.
27
27
 
28
28
  One short sentence on what you chose and why. Goes to telemetry and is shown back to the user. "watching CI run" beats "waiting." The user reads this to understand what you're doing without having to predict your cadence in advance — make it specific.
29
29
  `;
30
- function toSchedulerContext(c) {
30
+ function durableScopeOf(c) {
31
+ return c.principal ?? "default";
32
+ }
33
+ function toSchedulerContext(c, scope) {
31
34
  return {
32
- scope: c.scope,
35
+ scope,
33
36
  ...(c.principal !== undefined ? { principal: c.principal } : {}),
34
37
  ...(c.sessionId !== undefined ? { sessionId: c.sessionId } : {}),
35
38
  ...(c.taskConfig !== undefined ? { taskConfig: c.taskConfig } : {}),
@@ -157,7 +160,9 @@ export function createSchedulerTools(env, ctx) {
157
160
  if (!hasScheduler(env))
158
161
  return [];
159
162
  const sched = env;
160
- const schedCtx = toSchedulerContext(ctx);
163
+ const sessionCtx = toSchedulerContext(ctx, ctx.scope);
164
+ const durableCtx = toSchedulerContext(ctx, durableScopeOf(ctx));
165
+ const lanesDiffer = sessionCtx.scope !== durableCtx.scope;
161
166
  let cronCreateChain = Promise.resolve();
162
167
  const serializedCronCreateOp = (fn) => {
163
168
  const next = cronCreateChain.then(fn, fn);
@@ -255,9 +260,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
255
260
  ...(!durable ? { lifetime: "session" } : {}),
256
261
  ...(a.recurring !== undefined ? { recurring: a.recurring } : {}),
257
262
  };
258
- const before = await sched.list(schedCtx);
263
+ const opCtx = durable ? durableCtx : sessionCtx;
264
+ const before = await sched.list(opCtx);
259
265
  const knownIds = before.ok ? new Set(before.value.map((s) => s.id)) : undefined;
260
- const r = await sched.schedule(intent, schedCtx);
266
+ const r = await sched.schedule(intent, opCtx);
261
267
  if (!r.ok)
262
268
  return errorResult(`Error (CronCreate): ${r.error.message}`);
263
269
  const replaced = knownIds?.has(r.value.id);
@@ -285,6 +291,16 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
285
291
  };
286
292
  }),
287
293
  });
294
+ const cancelLanesFor = async (id) => {
295
+ if (!lanesDiffer)
296
+ return [sessionCtx];
297
+ for (const lane of [sessionCtx, durableCtx]) {
298
+ const listed = await sched.list(lane);
299
+ if (listed.ok && listed.value.some((s) => s.id === id))
300
+ return [lane];
301
+ }
302
+ return [sessionCtx, durableCtx];
303
+ };
288
304
  const cronCancel = defineTool({
289
305
  name: "CronDelete",
290
306
  contract: { contractId: "core.cron_delete@1", implementationRevision: "1" },
@@ -293,7 +309,14 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
293
309
  effect: "idempotent",
294
310
  execute: async (args) => {
295
311
  const { id } = args;
296
- const r = await sched.cancel(id, schedCtx);
312
+ const taskId = id;
313
+ const [firstLane, ...otherLanes] = await cancelLanesFor(taskId);
314
+ let r = await sched.cancel(taskId, firstLane);
315
+ for (const lane of otherLanes) {
316
+ if (r.ok || r.error.code !== "not_found")
317
+ break;
318
+ r = await sched.cancel(taskId, lane);
319
+ }
297
320
  if (!r.ok) {
298
321
  if (r.error.code === "not_found")
299
322
  return `CronDelete: no scheduled task "${id}" (already gone or not yours).`;
@@ -309,13 +332,23 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
309
332
  parameters: Type.Object({}),
310
333
  effect: "read",
311
334
  execute: async () => {
312
- const r = await sched.list(schedCtx);
313
- if (!r.ok)
314
- return errorResult(`Error (CronList): ${r.error.message}`);
315
- if (r.value.length === 0) {
335
+ const rows = [];
336
+ const seen = new Set();
337
+ for (const lane of lanesDiffer ? [sessionCtx, durableCtx] : [sessionCtx]) {
338
+ const listed = await sched.list(lane);
339
+ if (!listed.ok)
340
+ return errorResult(`Error (CronList): ${listed.error.message}`);
341
+ for (const s of listed.value) {
342
+ if (seen.has(s.id))
343
+ continue;
344
+ seen.add(s.id);
345
+ rows.push(s);
346
+ }
347
+ }
348
+ if (rows.length === 0) {
316
349
  return { content: "No scheduled tasks.", details: { type: "cron-list", jobs: [] } };
317
350
  }
318
- const content = r.value
351
+ const content = rows
319
352
  .map((s) => {
320
353
  const shown = cronExprFromSummary(s.when);
321
354
  const human = cronToHuman(s.when);
@@ -324,7 +357,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
324
357
  return `- ${s.id}: ${shown}${human !== s.when ? ` — ${human}` : ""}${s.label ? ` (${s.label})` : ""}${tier}${once}`;
325
358
  })
326
359
  .join("\n");
327
- const jobs = r.value.map((s) => {
360
+ const jobs = rows.map((s) => {
328
361
  const human = cronToHuman(s.when);
329
362
  return {
330
363
  id: s.id,
@@ -339,7 +372,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
339
372
  },
340
373
  });
341
374
  const listPendingWakeups = async () => {
342
- const r = await sched.list(schedCtx);
375
+ const r = await sched.list(sessionCtx);
343
376
  if (!r.ok)
344
377
  return { err: r.error.message };
345
378
  return {
@@ -351,7 +384,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
351
384
  const cancelWakeups = async (ids) => {
352
385
  let n = 0;
353
386
  for (const id of ids) {
354
- const r = await sched.cancel(id, schedCtx);
387
+ const r = await sched.cancel(id, sessionCtx);
355
388
  if (r.ok)
356
389
  n += 1;
357
390
  }
@@ -405,7 +438,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
405
438
  const pending = await listPendingWakeups();
406
439
  if (!("err" in pending))
407
440
  await cancelWakeups(pending.ids);
408
- const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" }, schedCtx);
441
+ const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" }, sessionCtx);
409
442
  if (!r.ok)
410
443
  return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`);
411
444
  const scheduledFor = Date.now() + clampedDelaySeconds * 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",