@sema-agent/core 5.6.0 → 5.7.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 +18 -0
- package/dist/core/checkpoint-store.d.ts +5 -2
- package/dist/core/checkpoint-store.js +6 -3
- package/dist/core/runner/runtask.js +67 -9
- package/dist/core/secret-env.d.ts +2 -0
- package/dist/core/secret-env.js +16 -4
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-notification.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.7.0 (2026-08-04)
|
|
4
|
+
|
|
5
|
+
_Three collected cars: a parked question is answered by the decision itself (behavior face — see the consumer note), a descendant's terminal notification anchors on the delegation tree, and the env scrub stops deleting infrastructure key names (behavior face)._
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A descendant background agent's terminal notification now anchors on the delegation tree instead of dying with its spawner. A background agent notifies through the injector of the run that spawned it, and that injector parked on that run's OWN session once its lane was gone — so a grandchild launched from a delegated run (root → child A → background grandchild B; A completes, B settles later) parked its terminal on a throwaway child session that never runs again, and the root conversation was never told (the data was never lost — `TaskOutput` still answered; the gap was purely the push face). A delegated agent's TERMINAL frame now also escalates one hop up the delegation chain: a live ancestor takes it in its current run, a dead one repeats the decision one level higher, and the walk ends at the tree root; without a chain link the recorded root anchor is used directly. Strictly additive: the own-session park is kept (a retained child session later resumed still receives what it always received), the live lane is untouched, and every other notification class (shell completions, SendMessage deliveries, external `notify()` events) stays addressed to its own session. A delegated run that can reach neither an uplink nor a root anchor still parks (fail-open) and discloses once (`degraded` / `descendant-terminal-unrouted`).
|
|
10
|
+
|
|
11
|
+
- The env scrub no longer deletes infrastructure key names. `SECRET_ENV_RE`'s bare `KEY` word matched `PARTITION_KEY` / `SORT_KEY` / `RANGE_KEY` / `IDEMPOTENCY_KEY` / `CACHE_KEY` / `ROUTING_KEY`, and `scrubSecretEnv` DELETES what it matches — a spawned child silently lost variables that carry no credential (writes to a wrong partition, a routing key nothing binds). The word itself cannot be withdrawn (`API_KEY` / `SECRET_KEY` / `PRIVATE_KEY` / `ACCESS_KEY` are the bulk of real credential names), so the fix is the mirror of the existing positive exact-name list: `NON_SECRET_ENV_EXACT_NAMES`, exact names (never a shape — a pattern exemption would be a bypass) confirmed non-credential, consulted before the shape rule with the credential list read first (a name on both lists resolves to the credential reading). An exempted entry is kept and mints no finding. Decorated variants (`USERS_SORT_KEY`, `SORT_KEY_2`) are still dropped. **Consumers pinning the old shape**: a probe asserting `isSecretEnvKey("SORT_KEY") === true`, or that a scrub drops one of the six exempt names, now reads the opposite answer.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A durable question can be answered by the decision itself. `ResumeOutcome`'s `policy_ask` arm takes an optional `answer` (a `QuestionAnswer`) when the checkpoint's pending call is the reserved `AskUserQuestion` tool; the resume binds it as the resumed leg's answering face, so the pending call executes against the operator's actual selections. This is the same wiring the documented live arm builds by hand (`resume(token, outcome, { ...config, onQuestion: async () => answer })`) — but minted from the payload, which is the only route available to a caller redeeming an **offline background child**: the revive drive rebuilds that leg's config from the parked row and has no seat to put a closure in, so approving a parked question used to feed the model a fabricated "no human is available" default. The answer is untrusted wire data throughout: it goes through the existing answer fence unchanged (`selected ⊆ options` for the trusted line, off-list values and free-text `note` in a data fence), so multi-select and "Other" free text behave identically on both arms. The binding is one-shot — it answers the decided call only; a new question on the resumed leg is not fed the old answer. The answer is also recorded on the persisted winner, so an `env_failed` reopen must replay the identical answer (a replay may not keep the verdict and swap the text).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Behavior face** — approving a durable question without an answer is now refused instead of consumed. A `policy_ask` `allow` whose pending call is the reserved question tool is rejected **pre-CAS** (`checkpoint.invalid_outcome`, `detail.field: "answer"`, the checkpoint stays `pending` and redeemable) when it carries neither an `answer` nor a live answering face — where "live" excludes the reserved `QUESTION_AWAITS_RESUME` placeholder, whose contract is that it must never run. Previously that shape consumed the human's approval and let the run finish on a fabricated default (or, with the placeholder, on its config error) with nothing left to redeem. A resume that supplies its own real `onQuestion` is unaffected; so is every non-question approval. Symmetrically, an `answer` attached to a decision that cannot carry one (a `deny`, or a pending call that is not the question tool) is rejected the same way rather than silently dropped. **Consumers pinning the old shape**: a probe that approved a parked question with a bare allow now gets a typed rejection — supply the answer on the outcome, or deny.
|
|
20
|
+
|
|
3
21
|
## 5.6.0 (2026-08-03)
|
|
4
22
|
|
|
5
23
|
_A hardening tail: numeric knobs refuse what they cannot honor, the binary gate reads bytes instead of trusting statistics, and the MCP SDK takes its last zero-cost step before the v2 jump._
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type QuestionAnswer } from "./ask-question.js";
|
|
1
2
|
import type { ReadEntry } from "../tools/fs/safety.js";
|
|
2
3
|
import type { RepairBundle } from "../agents/repair-loop.js";
|
|
3
4
|
import type { WorkspaceHandle } from "./remote-env.js";
|
|
@@ -68,6 +69,7 @@ export type ResumeOutcome = {
|
|
|
68
69
|
decision: "allow" | "deny";
|
|
69
70
|
updatedInput?: unknown;
|
|
70
71
|
reason?: string;
|
|
72
|
+
answer?: QuestionAnswer;
|
|
71
73
|
} | {
|
|
72
74
|
gate: "resource_limit";
|
|
73
75
|
decision: "continue";
|
|
@@ -180,6 +182,7 @@ export interface ResolvedOutcome {
|
|
|
180
182
|
boundCallId: string;
|
|
181
183
|
decision: "allow" | "deny" | "approve" | "reject" | "edit";
|
|
182
184
|
updatedInput?: unknown;
|
|
185
|
+
answer?: QuestionAnswer;
|
|
183
186
|
}
|
|
184
187
|
export type ReopenReason = "env_failed" | "tool_unavailable";
|
|
185
188
|
export interface ResolveExpectation {
|
|
@@ -241,10 +244,10 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
|
|
|
241
244
|
export declare class CheckpointError extends Error {
|
|
242
245
|
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch";
|
|
243
246
|
readonly detail?: {
|
|
244
|
-
field?: "boundCallId" | "boundInputHash";
|
|
247
|
+
field?: "boundCallId" | "boundInputHash" | "answer";
|
|
245
248
|
} | undefined;
|
|
246
249
|
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string, detail?: {
|
|
247
|
-
field?: "boundCallId" | "boundInputHash";
|
|
250
|
+
field?: "boundCallId" | "boundInputHash" | "answer";
|
|
248
251
|
} | undefined);
|
|
249
252
|
}
|
|
250
253
|
export interface CheckpointStore {
|
|
@@ -223,9 +223,12 @@ export function winnerFromOutcome(outcome) {
|
|
|
223
223
|
}
|
|
224
224
|
if (outcome.gate !== "policy_ask")
|
|
225
225
|
return undefined;
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
return {
|
|
227
|
+
boundCallId: outcome.boundCallId,
|
|
228
|
+
decision: outcome.decision,
|
|
229
|
+
...(outcome.updatedInput === undefined ? {} : { updatedInput: outcome.updatedInput }),
|
|
230
|
+
...(outcome.answer === undefined ? {} : { answer: outcome.answer }),
|
|
231
|
+
};
|
|
229
232
|
}
|
|
230
233
|
export function validatePendingSteer(steer) {
|
|
231
234
|
if (sanitizeUntrustedText(steer.text) !== steer.text) {
|
|
@@ -5,7 +5,7 @@ import { engineVersion } from "../version.js";
|
|
|
5
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
6
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
7
7
|
import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, isCompactionManualCancel, isCompactionWalltimeAbort, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
|
|
8
|
-
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
8
|
+
import { ASK_USER_QUESTION_TOOL_NAME, QUESTION_AWAITS_RESUME } from "../ask-question.js";
|
|
9
9
|
import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
|
|
10
10
|
import { emitTrace } from "../trace.js";
|
|
11
11
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
@@ -38,7 +38,7 @@ import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
|
38
38
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
39
39
|
import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
|
|
40
40
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
41
|
-
import { discloseDroppedPending, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
41
|
+
import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
42
42
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
43
43
|
import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
|
|
44
44
|
import { DEFAULT_MAX_TURNS } from "../../config/defaults.js";
|
|
@@ -135,7 +135,24 @@ function deepJsonEqual(a, b) {
|
|
|
135
135
|
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
|
|
136
136
|
}
|
|
137
137
|
function sameWinner(a, b) {
|
|
138
|
-
return a.boundCallId === b.boundCallId &&
|
|
138
|
+
return (a.boundCallId === b.boundCallId &&
|
|
139
|
+
a.decision === b.decision &&
|
|
140
|
+
deepJsonEqual(a.updatedInput, b.updatedInput) &&
|
|
141
|
+
deepJsonEqual(a.answer, b.answer));
|
|
142
|
+
}
|
|
143
|
+
function pendingContentAskCallId(cp) {
|
|
144
|
+
return cp.pendingAction.kind === "tool_approval" && cp.pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME
|
|
145
|
+
? cp.pendingAction.toolCallId
|
|
146
|
+
: undefined;
|
|
147
|
+
}
|
|
148
|
+
function answerFaceForRedeemedCall(answer, redeemedCallId, base) {
|
|
149
|
+
return async (req, signal) => {
|
|
150
|
+
if (req.toolCallId === redeemedCallId)
|
|
151
|
+
return answer;
|
|
152
|
+
if (base !== undefined)
|
|
153
|
+
return base(req, signal);
|
|
154
|
+
throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
|
|
155
|
+
};
|
|
139
156
|
}
|
|
140
157
|
function writeFamilyOfCanonical(name) {
|
|
141
158
|
if (name === "TaskCreate" || name === "TaskUpdate")
|
|
@@ -1559,6 +1576,32 @@ export class Runner {
|
|
|
1559
1576
|
let notificationIdent = () => ({});
|
|
1560
1577
|
let notificationLaneLive = true;
|
|
1561
1578
|
let notificationSessionId;
|
|
1579
|
+
let descendantAnchorDisclosed = false;
|
|
1580
|
+
const parkTaskNotification = (payload, priority) => {
|
|
1581
|
+
if (notificationSessionId !== undefined)
|
|
1582
|
+
this.pendingSessionNotifications.pend(notificationSessionId, payload);
|
|
1583
|
+
if (!isDelegatedAgentTerminal(payload))
|
|
1584
|
+
return;
|
|
1585
|
+
const uplink = internals?.parentNotify;
|
|
1586
|
+
if (uplink !== undefined) {
|
|
1587
|
+
try {
|
|
1588
|
+
uplink(payload, { priority });
|
|
1589
|
+
}
|
|
1590
|
+
catch (e) {
|
|
1591
|
+
this.deps.onError?.(e, { phase: "degraded", sessionId: notificationSessionId ?? "", classification: "descendant-terminal-uplink" });
|
|
1592
|
+
}
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const rootAnchor = internals?.rootSessionId;
|
|
1596
|
+
if (rootAnchor !== undefined && rootAnchor !== notificationSessionId) {
|
|
1597
|
+
this.pendingSessionNotifications.pend(rootAnchor, payload);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (internals?.parentSessionId !== undefined && !descendantAnchorDisclosed) {
|
|
1601
|
+
descendantAnchorDisclosed = true;
|
|
1602
|
+
this.deps.onError?.(new Error(`background-agent terminal ${payload.task_id} parked on this run's own session — the delegation tree's root session is not reachable from this run (no uplink, no root anchor)`), { phase: "degraded", sessionId: notificationSessionId ?? "", classification: "descendant-terminal-unrouted" });
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1562
1605
|
const unsubscribeTaskNotifications = taskNotificationQueue.subscribe((item) => {
|
|
1563
1606
|
queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
|
|
1564
1607
|
if (notificationHarness) {
|
|
@@ -1567,14 +1610,12 @@ export class Runner {
|
|
|
1567
1610
|
? notificationHarness.followUp(xml, { provenance: "engine-note", enginePayload: item.payload })
|
|
1568
1611
|
: notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
|
|
1569
1612
|
void deliver.then(() => item.onDisposition?.("queued"), () => {
|
|
1570
|
-
|
|
1571
|
-
this.pendingSessionNotifications.pend(notificationSessionId, item.payload);
|
|
1613
|
+
parkTaskNotification(item.payload, item.priority);
|
|
1572
1614
|
item.onDisposition?.("parked");
|
|
1573
1615
|
});
|
|
1574
1616
|
}
|
|
1575
1617
|
else {
|
|
1576
|
-
|
|
1577
|
-
this.pendingSessionNotifications.pend(notificationSessionId, item.payload);
|
|
1618
|
+
parkTaskNotification(item.payload, item.priority);
|
|
1578
1619
|
item.onDisposition?.("parked");
|
|
1579
1620
|
}
|
|
1580
1621
|
});
|
|
@@ -1584,8 +1625,7 @@ export class Runner {
|
|
|
1584
1625
|
if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
|
|
1585
1626
|
return Promise.resolve("dropped_duplicate");
|
|
1586
1627
|
if (!notificationLaneLive) {
|
|
1587
|
-
|
|
1588
|
-
this.pendingSessionNotifications.pend(notificationSessionId, notification);
|
|
1628
|
+
parkTaskNotification(notification, opts?.priority ?? "later");
|
|
1589
1629
|
return Promise.resolve("parked");
|
|
1590
1630
|
}
|
|
1591
1631
|
return new Promise((resolve) => {
|
|
@@ -3240,6 +3280,20 @@ export class Runner {
|
|
|
3240
3280
|
throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
|
|
3241
3281
|
}
|
|
3242
3282
|
}
|
|
3283
|
+
const contentAskCallId = pendingContentAskCallId(cp);
|
|
3284
|
+
if (outcome.answer !== undefined && (contentAskCallId === undefined || outcome.decision !== "allow")) {
|
|
3285
|
+
throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
|
|
3286
|
+
? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
|
|
3287
|
+
: "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
|
|
3288
|
+
}
|
|
3289
|
+
if (contentAskCallId !== undefined && outcome.decision === "allow" && outcome.answer === undefined) {
|
|
3290
|
+
const liveFace = taskConfig.onQuestion ?? this.deps.onQuestion;
|
|
3291
|
+
if (liveFace === undefined || liveFace === QUESTION_AWAITS_RESUME) {
|
|
3292
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
|
|
3293
|
+
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
3294
|
+
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3243
3297
|
}
|
|
3244
3298
|
if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
3245
3299
|
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`);
|
|
@@ -3279,12 +3333,16 @@ export class Runner {
|
|
|
3279
3333
|
await internals.afterCheckpointResolve();
|
|
3280
3334
|
consumeFlipDone = true;
|
|
3281
3335
|
}
|
|
3336
|
+
const answerFace = outcome.gate === "policy_ask" && outcome.answer !== undefined
|
|
3337
|
+
? answerFaceForRedeemedCall(outcome.answer, outcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion)
|
|
3338
|
+
: undefined;
|
|
3282
3339
|
const spec = {
|
|
3283
3340
|
...taskConfig,
|
|
3284
3341
|
objective: "",
|
|
3285
3342
|
sessionId: cp.sessionId,
|
|
3286
3343
|
requireExistingSession: true,
|
|
3287
3344
|
preemptSignal: taskConfig.preemptSignal?.aborted ? undefined : taskConfig.preemptSignal,
|
|
3345
|
+
...(answerFace !== undefined ? { onQuestion: answerFace } : {}),
|
|
3288
3346
|
};
|
|
3289
3347
|
const reopenFn = store.reopen?.bind(store);
|
|
3290
3348
|
const onEnvRestoreFailed = reopenFn
|
|
@@ -6,5 +6,7 @@ export interface SecretEnvFinding {
|
|
|
6
6
|
confidence: RedactionConfidence;
|
|
7
7
|
source: RedactionSource;
|
|
8
8
|
}
|
|
9
|
+
export type ExactEnvNameVerdict = "credential" | "exempt" | "unknown";
|
|
10
|
+
export declare function classifyExactEnvName(key: string, credentialNames: ReadonlySet<string>, exemptNames: ReadonlySet<string>): ExactEnvNameVerdict;
|
|
9
11
|
export declare function isSecretEnvKey(key: string): boolean;
|
|
10
12
|
export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
|
package/dist/core/secret-env.js
CHANGED
|
@@ -4,16 +4,28 @@ 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 NON_SECRET_ENV_EXACT_NAMES = new Set([
|
|
8
|
+
"PARTITION_KEY", "SORT_KEY", "RANGE_KEY", "IDEMPOTENCY_KEY", "CACHE_KEY", "ROUTING_KEY",
|
|
9
|
+
]);
|
|
7
10
|
const SECRET_ENV_KIND_CONFIDENCE = {
|
|
8
11
|
"exact-name": "high",
|
|
9
12
|
"suffix-rule": "medium",
|
|
10
13
|
};
|
|
14
|
+
export function classifyExactEnvName(key, credentialNames, exemptNames) {
|
|
15
|
+
const upper = key.toUpperCase();
|
|
16
|
+
if (credentialNames.has(upper))
|
|
17
|
+
return "credential";
|
|
18
|
+
if (exemptNames.has(upper))
|
|
19
|
+
return "exempt";
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
11
22
|
function classifySecretEnvKey(key) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (SECRET_ENV_EXACT_NAMES.has(key.toUpperCase()))
|
|
23
|
+
const byExactName = classifyExactEnvName(key, SECRET_ENV_EXACT_NAMES, NON_SECRET_ENV_EXACT_NAMES);
|
|
24
|
+
if (byExactName === "credential")
|
|
15
25
|
return "exact-name";
|
|
16
|
-
|
|
26
|
+
if (byExactName === "exempt")
|
|
27
|
+
return undefined;
|
|
28
|
+
return SECRET_ENV_RE.test(key) ? "suffix-rule" : undefined;
|
|
17
29
|
}
|
|
18
30
|
export function isSecretEnvKey(key) {
|
|
19
31
|
return classifySecretEnvKey(key) !== undefined;
|
|
@@ -45,6 +45,7 @@ export declare function renderTaskNotificationXml(n: TaskNotificationPayload): s
|
|
|
45
45
|
export declare const MAX_PENDING_EVENTS_PER_TASK = 50;
|
|
46
46
|
export declare const MAX_PENDING_EVENTS_PER_SESSION = 200;
|
|
47
47
|
export declare const MAX_PENDING_SESSIONS = 100;
|
|
48
|
+
export declare function isDelegatedAgentTerminal(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
|
|
48
49
|
export interface DrainedPendingNotifications {
|
|
49
50
|
items: TaskNotificationPayload[];
|
|
50
51
|
dropped: Map<string, {
|
|
@@ -83,6 +83,9 @@ export const MAX_PENDING_EVENTS_PER_SESSION = 200;
|
|
|
83
83
|
export const MAX_PENDING_SESSIONS = 100;
|
|
84
84
|
const MAX_TOMBSTONES = 5_000;
|
|
85
85
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "killed", "cancelled"]);
|
|
86
|
+
export function isDelegatedAgentTerminal(n) {
|
|
87
|
+
return n.task_type === "background_agent" && TERMINAL_STATUSES.has(n.status);
|
|
88
|
+
}
|
|
86
89
|
export class PendingSessionNotifications {
|
|
87
90
|
sessions = new Map();
|
|
88
91
|
droppedSessions = 0;
|