@sema-agent/core 7.3.0 → 7.4.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 +49 -0
- package/dist/agents/peer-admission.d.ts +18 -3
- package/dist/agents/peer-admission.js +79 -4
- package/dist/agents/peer-held-queue.d.ts +101 -0
- package/dist/agents/peer-held-queue.js +229 -0
- package/dist/agents/peer-idle.d.ts +109 -0
- package/dist/agents/peer-idle.js +240 -0
- package/dist/agents/peer-notice-route.d.ts +33 -0
- package/dist/agents/peer-notice-route.js +46 -0
- package/dist/agents/peer-notices.d.ts +103 -0
- package/dist/agents/peer-notices.js +206 -0
- package/dist/agents/peer-session-drain.d.ts +39 -4
- package/dist/agents/peer-session-drain.js +248 -42
- package/dist/agents/send-message-tool.d.ts +8 -1
- package/dist/agents/send-message-tool.js +96 -30
- package/dist/agents/subagent.js +1 -0
- package/dist/brain/status-sink.d.ts +10 -0
- package/dist/brain/status-sink.js +13 -4
- package/dist/brain/stream-engine.d.ts +11 -0
- package/dist/brain/stream-engine.js +39 -3
- package/dist/core/arg-summary.d.ts +13 -3
- package/dist/core/arg-summary.js +138 -7
- package/dist/core/auto-mode-defaults.d.ts +11 -0
- package/dist/core/auto-mode-defaults.js +2 -0
- package/dist/core/auto-mode.d.ts +59 -0
- package/dist/core/auto-mode.js +57 -1
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hooks.d.ts +30 -0
- package/dist/core/hooks.js +43 -8
- package/dist/core/mailbox-store.d.ts +33 -1
- package/dist/core/mailbox-store.js +42 -2
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/denial-limit-arms.d.ts +149 -0
- package/dist/core/runner/denial-limit-arms.js +91 -0
- package/dist/core/runner/edited-files-ledger.d.ts +33 -0
- package/dist/core/runner/edited-files-ledger.js +14 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +62 -1
- package/dist/core/runner/prepare-task.js +135 -89
- package/dist/core/runner/runtask.js +12 -0
- package/dist/core/sensitive-path-policy.d.ts +27 -6
- package/dist/core/sensitive-path-policy.js +57 -2
- package/dist/core/task-notification.d.ts +24 -2
- package/dist/core/task-notification.js +6 -1
- package/dist/core/tool-policy.d.ts +55 -4
- package/dist/core/tool-policy.js +28 -5
- package/dist/core/tools.js +1 -0
- package/dist/core/types.d.ts +251 -15
- package/dist/core/wiring-manifest.d.ts +41 -5
- package/dist/core/wiring-manifest.js +8 -0
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +3 -0
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +7 -0
- package/dist/engine/loop/agent-loop.js +79 -0
- package/dist/engine/loop/types.d.ts +42 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +10 -4
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/workflow.js +7 -3
- package/dist/tools/fs/fs-write.d.ts +4 -4
- package/dist/tools/fs/fs-write.js +99 -14
- package/dist/tools/fs/index.d.ts +7 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/safety.d.ts +29 -8
- package/dist/tools/fs/safety.js +11 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +181 -1
|
@@ -6,6 +6,8 @@ import { uuidv7 } from "../internal/harness.js";
|
|
|
6
6
|
import { MAILBOX_TOMBSTONED_RECIPIENT_CODE, mailboxCrossProcessMountVerdict } from "../core/mailbox-store.js";
|
|
7
7
|
import { peerSendVerdictSeverity } from "./cross-session-judge.js";
|
|
8
8
|
import { parsePeerSessionAddress, peerSessionBoxHandle, resolvePeerSessions } from "./peer-directory.js";
|
|
9
|
+
import { peerIdleMachineFor } from "./peer-idle.js";
|
|
10
|
+
import { PEER_IDLE_OUTSTANDING_CAP, PEER_IDLE_SUBSCRIPTION_TTL_MS, peerIdleNoticeLabel } from "./peer-notices.js";
|
|
9
11
|
import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
10
12
|
import { inlineUntrusted } from "../core/untrusted-text.js";
|
|
11
13
|
import { neutralizePeerBody } from "./cross-session-envelope.js";
|
|
@@ -50,6 +52,11 @@ function targetLaneKey(scope, targetId) {
|
|
|
50
52
|
const OPERATOR_CONTINUATION_CTX = Symbol("sema.operator_continuation");
|
|
51
53
|
export function createSendMessageTool(opts) {
|
|
52
54
|
const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
|
|
55
|
+
const notifyWhenIdleParameter = {
|
|
56
|
+
notify_when_idle: Type.Optional(Type.Boolean({
|
|
57
|
+
description: "Ask a peer SESSION of this engine to send you ONE notice when it next goes idle (finishes its turn with nothing queued) or exits — opt-in, one-shot, no polling. With a message: deliver it now AND subscribe. Without a message (omit it): a pure subscription that costs the other session nothing. Peer sessions only (not agents or teammates), and only from the main conversation.",
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
53
60
|
const peerLane = (() => {
|
|
54
61
|
if (opts.peerDirectory === undefined)
|
|
55
62
|
return { active: false };
|
|
@@ -58,6 +65,14 @@ export function createSendMessageTool(opts) {
|
|
|
58
65
|
const verdict = mailboxCrossProcessMountVerdict(opts.mailbox);
|
|
59
66
|
return verdict.ok ? { active: true } : { active: false, reason: verdict.reason };
|
|
60
67
|
})();
|
|
68
|
+
const baseParameters = Type.Object({
|
|
69
|
+
to: Type.String({ description: 'Recipient: the agent\'s name, or its task_id (a…) returned by the Agent tool with run_in_background. "main" is reserved for the spawning conversation.' }),
|
|
70
|
+
message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
|
|
71
|
+
summary: Type.Optional(Type.String({ maxLength: 200, description: "A 5-10 word summary shown as a preview in the UI (required when message is a string)" })),
|
|
72
|
+
});
|
|
73
|
+
const sendMessageParameters = peerLane.active
|
|
74
|
+
? Type.Object({ ...baseParameters.properties, message: Type.Optional(Type.String({ description: `${baseParameters.properties.message.description ?? ""} Optional only for a pure notify_when_idle subscription.`.trim() })), ...notifyWhenIdleParameter })
|
|
75
|
+
: baseParameters;
|
|
61
76
|
const retrievalMounted = opts.retrievalToolMounted;
|
|
62
77
|
const retrievalHedge = retrievalMounted === undefined ? " (where that tool is mounted)" : "";
|
|
63
78
|
const RETRIEVE_NOW = retrievalMounted === false
|
|
@@ -144,19 +159,16 @@ export function createSendMessageTool(opts) {
|
|
|
144
159
|
(peerLane.active
|
|
145
160
|
? ` Peer SESSIONS — other sessions of this engine for the same user${opts.peerListingMounted === false ? "" : opts.peerListingMounted === true ? " (list them with ListAgents)" : " (list them with ListAgents where that tool is mounted)"} — are addressed by name, by \`name [ref]\` when a name is ambiguous, or by their \`session.<id>\` address. A message to a peer is parked durably in its session box and read at its next turn boundary (an offline peer reads it when it next runs); the receipt confirms the box accepted it, not that the peer acted on it.`
|
|
146
161
|
: ""),
|
|
147
|
-
parameters:
|
|
148
|
-
to: Type.String({ description: 'Recipient: the agent\'s name, or its task_id (a…) returned by the Agent tool with run_in_background. "main" is reserved for the spawning conversation.' }),
|
|
149
|
-
message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
|
|
150
|
-
summary: Type.Optional(Type.String({ maxLength: 200, description: "A 5-10 word summary shown as a preview in the UI (required when message is a string)" })),
|
|
151
|
-
}),
|
|
162
|
+
parameters: sendMessageParameters,
|
|
152
163
|
execute: async (args, ctx) => {
|
|
153
164
|
const a = args;
|
|
154
165
|
const operatorUserInitiated = ctx[OPERATOR_CONTINUATION_CTX] === true;
|
|
155
166
|
const to = String(a.to ?? "").trim();
|
|
156
167
|
const message = String(a.message ?? "").trim();
|
|
168
|
+
const notifyWhenIdle = a.notify_when_idle === true;
|
|
157
169
|
if (!to)
|
|
158
170
|
return { content: "Message not sent: 'to' was empty. Pass the agent's task_id (a…).", details: { error: "empty to" }, isError: true };
|
|
159
|
-
if (!message)
|
|
171
|
+
if (!message && !notifyWhenIdle)
|
|
160
172
|
return { content: "Message not sent: 'message' was empty.", details: { error: "empty message" }, isError: true };
|
|
161
173
|
if (to === "*") {
|
|
162
174
|
return {
|
|
@@ -173,7 +185,7 @@ export function createSendMessageTool(opts) {
|
|
|
173
185
|
};
|
|
174
186
|
}
|
|
175
187
|
const summaryArg = typeof a.summary === "string" ? a.summary.trim() : "";
|
|
176
|
-
if (summaryArg === "") {
|
|
188
|
+
if (summaryArg === "" && message !== "") {
|
|
177
189
|
return {
|
|
178
190
|
content: "Message not sent: summary is required when message is a string — pass a 5-10 word summary of the follow-up.",
|
|
179
191
|
details: { error: "summary_required", to },
|
|
@@ -192,7 +204,7 @@ export function createSendMessageTool(opts) {
|
|
|
192
204
|
if (isObserverTaskId(to)) {
|
|
193
205
|
return { content: OBSERVER_SENDMESSAGE_TARGET_REFUSAL, details: { error: "observer_target", to }, isError: true };
|
|
194
206
|
}
|
|
195
|
-
const admissionConfig = resolvePeerAdmissionConfig(opts.admission);
|
|
207
|
+
const admissionConfig = resolvePeerAdmissionConfig(opts.admission, opts.onNotice);
|
|
196
208
|
const guardScope = ctx.principal ?? opts.scope ?? opts.peerSelfSession?.scope;
|
|
197
209
|
const selfRef = ctx.peerSelfRef ?? opts.peerSelf;
|
|
198
210
|
const directMountSessionId = ctx.sessionId ?? opts.sessionId ?? opts.peerSelfSession?.sessionId;
|
|
@@ -244,11 +256,8 @@ export function createSendMessageTool(opts) {
|
|
|
244
256
|
if (record.inboundPosture === "unavailable") {
|
|
245
257
|
return peerRefusal("peer_send.recipient_refuses", `${peerLabel} advertises that it does not accept cross-session messages (its crossSessionInbound is "refuse"). Nothing was parked.`);
|
|
246
258
|
}
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const sendVerdict = judgePeerAdmission(peerScope, recipientKey, { senderKey, body: storedContent, prospectiveChain, ownTokens: [recipientKey] }, admissionConfig, undefined, "send");
|
|
250
|
-
if (!sendVerdict.ok)
|
|
251
|
-
return admissionRefusal(sendVerdict.reason, peerLabel);
|
|
259
|
+
const modeClass = opts.peerSelfSession?.modeClass?.();
|
|
260
|
+
const now = Date.now();
|
|
252
261
|
let boxFull = false;
|
|
253
262
|
try {
|
|
254
263
|
boxFull = (await opts.mailbox.peekCount(peerScope, handle)) >= admissionConfig.maxQueuedPeerMessages;
|
|
@@ -258,23 +267,56 @@ export function createSendMessageTool(opts) {
|
|
|
258
267
|
if (boxFull) {
|
|
259
268
|
return { content: `Message not sent: ${inlineUntrusted(peerLabel, 200)}'s session box is at its queued-message limit (${admissionConfig.maxQueuedPeerMessages.toFixed(0)}) — the message was NOT queued; it drains the box at its next turn. Resend later. ${DEDUP_RETRY_NOTE.slice(0, 200)}`, details: { error: "queue_full", code: "queue_full", to }, isError: true };
|
|
260
269
|
}
|
|
261
|
-
const modeClass = opts.peerSelfSession?.modeClass?.();
|
|
262
|
-
const peerMeta = {
|
|
263
|
-
...(peerSelfSessionId !== undefined ? { fromSession: peerSelfSessionId } : {}),
|
|
264
|
-
...(modeClass === "bypass" || modeClass === "prompting" ? { fromMode: modeClass } : {}),
|
|
265
|
-
...(senderKey !== undefined ? { senderKey } : {}),
|
|
266
|
-
kind: "peer_message",
|
|
267
|
-
};
|
|
268
|
-
const now = Date.now();
|
|
269
270
|
let seq;
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
if (message !== "") {
|
|
272
|
+
const recipientKey = peerAxisToken(peerScope, "s", record.sessionId);
|
|
273
|
+
const storedContent = `[${summary}] ${clipCarrierMessage(message)}`;
|
|
274
|
+
const sendVerdict = judgePeerAdmission(peerScope, recipientKey, { senderKey, body: storedContent, prospectiveChain, ownTokens: [recipientKey] }, admissionConfig, undefined, "send");
|
|
275
|
+
if (!sendVerdict.ok)
|
|
276
|
+
return admissionRefusal(sendVerdict.reason, peerLabel);
|
|
277
|
+
const peerMeta = {
|
|
278
|
+
...(peerSelfSessionId !== undefined ? { fromSession: peerSelfSessionId } : {}),
|
|
279
|
+
...(modeClass === "bypass" || modeClass === "prompting" ? { fromMode: modeClass } : {}),
|
|
280
|
+
...(senderKey !== undefined ? { senderKey } : {}),
|
|
281
|
+
kind: "peer_message",
|
|
282
|
+
};
|
|
283
|
+
try {
|
|
284
|
+
seq = await opts.mailbox.append(peerScope, handle, { from: ownRowName ?? opts.peerSelfSession?.name ?? senderLabel, content: storedContent, sentAt: now, hopChain: prospectiveChain, peerMeta });
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
if (e?.code === MAILBOX_TOMBSTONED_RECIPIENT_CODE) {
|
|
288
|
+
return peerRefusal("peer_send.invalid_target", `${peerLabel} is being deleted — its box no longer accepts messages. Nothing was parked; do not retry.`);
|
|
289
|
+
}
|
|
290
|
+
return peerRefusal("peer_send.other", `the session box refused the message (${e instanceof Error ? e.message : String(e)}) — nothing was parked. ${DEDUP_RETRY_NOTE}`);
|
|
291
|
+
}
|
|
272
292
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
293
|
+
let subscription;
|
|
294
|
+
let subscriptionSeq;
|
|
295
|
+
if (notifyWhenIdle) {
|
|
296
|
+
if (peerSelfSessionId === undefined) {
|
|
297
|
+
subscription = { ok: false, code: "peer_send.other", severity: "error", message: "this mount has no session identity, so no idle notice could be addressed back to it — no subscription was made." };
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const ask = peerIdleMachineFor(peerScope, peerSelfSessionId).requester.request(record.sessionId, peerIdleNoticeLabel(record.name) ?? handle);
|
|
301
|
+
if (!ask.ok) {
|
|
302
|
+
subscription = { ok: false, code: "peer_send.subscription_cap", severity: peerSendVerdictSeverity("peer_send.subscription_cap"), message: `you already hold ${PEER_IDLE_OUTSTANDING_CAP} outstanding idle subscriptions — no new one was made; wait for a notice or for one to expire.` };
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const subMeta = {
|
|
306
|
+
fromSession: peerSelfSessionId,
|
|
307
|
+
...(modeClass === "bypass" || modeClass === "prompting" ? { fromMode: modeClass } : {}),
|
|
308
|
+
kind: "idle_subscription",
|
|
309
|
+
};
|
|
310
|
+
try {
|
|
311
|
+
subscriptionSeq = await opts.mailbox.append(peerScope, handle, { from: ownRowName ?? opts.peerSelfSession?.name ?? senderLabel, content: `[idle subscription] ${ownRowName ?? opts.peerSelfSession?.name ?? senderLabel} asked to be told once when this session next goes idle`, sentAt: now, hopChain: [], peerMeta: subMeta });
|
|
312
|
+
subscription = { ok: true, disposition: record.liveness !== "live" ? "parked_offline" : "queued", via: "store" };
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
peerIdleMachineFor(peerScope, peerSelfSessionId).requester.settle(record.sessionId, "unavailable");
|
|
316
|
+
subscription = { ok: false, code: e?.code === MAILBOX_TOMBSTONED_RECIPIENT_CODE ? "peer_send.invalid_target" : "peer_send.other", severity: "error", message: `the session box refused the subscription record (${(e instanceof Error ? e.message : String(e)).slice(0, 200)}) — no subscription was made.` };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
276
319
|
}
|
|
277
|
-
return peerRefusal("peer_send.other", `the session box refused the message (${e instanceof Error ? e.message : String(e)}) — nothing was parked. ${DEDUP_RETRY_NOTE}`);
|
|
278
320
|
}
|
|
279
321
|
let rowRestored = "present";
|
|
280
322
|
try {
|
|
@@ -294,12 +336,26 @@ export function createSendMessageTool(opts) {
|
|
|
294
336
|
? " Note: that session was DELETED while this message was being parked — its box is being retired and this message will not be read; do not resend to it."
|
|
295
337
|
: "";
|
|
296
338
|
const offline = record.liveness !== "live";
|
|
339
|
+
const subscriptionNote = subscription === undefined
|
|
340
|
+
? ""
|
|
341
|
+
: subscription.ok
|
|
342
|
+
? ` Idle subscription recorded (one-shot, expires unheard after ${PEER_IDLE_SUBSCRIPTION_TTL_MS / 3_600_000} h): you will get ONE automated notice when that session next goes idle or exits${offline ? " — it is offline now, so the ask waits in its box with the message" : ""}. Do not poll for it.`
|
|
343
|
+
: ` Idle subscription NOT made (${subscription.code}): ${subscription.message}`;
|
|
344
|
+
if (seq === undefined) {
|
|
345
|
+
if (subscription === undefined || !subscription.ok) {
|
|
346
|
+
return { content: `Subscription not made: ${subscription?.message ?? "nothing to do"}`, details: { error: subscription?.code ?? "peer_send.other", code: subscription?.code ?? "peer_send.other", to, ...(subscription !== undefined ? { subscription } : {}) }, isError: true };
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
content: `Idle subscription recorded for ${peerLabel} (record seq ${subscriptionSeq}): you will get ONE automated notice when that session next goes idle or exits (one-shot; it expires unheard after ${PEER_IDLE_SUBSCRIPTION_TTL_MS / 3_600_000} h). No message was sent. Do not poll for it; continue with your task.${rowNote}`,
|
|
350
|
+
details: { type: "send-message", status: "peer_idle_subscribed", to, address: handle, subscription, ...(subscriptionSeq !== undefined ? { subscriptionSeq } : {}), ...(rowRestored !== "present" ? { directoryRow: rowRestored } : {}) },
|
|
351
|
+
};
|
|
352
|
+
}
|
|
297
353
|
const verdict = { ok: true, disposition: offline ? "parked_offline" : "queued", via: "store" };
|
|
298
354
|
return {
|
|
299
355
|
content: offline
|
|
300
|
-
? `Message parked for ${peerLabel}: that session is offline, so the message was parked durably in its session box (seq ${seq}) and will be delivered when it next runs — subject to the deployment's mailbox retirement policy, which does not notify you. Continue with your task; do not resend the same content.${rowNote}`
|
|
301
|
-
: `Message queued for ${peerLabel} (seq ${seq}): it is parked durably in that session's box and will be read at its next turn boundary. This receipt confirms acceptance into the box, not the peer's judgment of the message. Continue with your task; do not resend the same content.${rowNote}`,
|
|
302
|
-
details: { type: "send-message", status: offline ? "peer_parked_offline" : "peer_queued", to, address: handle, seq, verdict, ...(rowRestored !== "present" ? { directoryRow: rowRestored } : {}) },
|
|
356
|
+
? `Message parked for ${peerLabel}: that session is offline, so the message was parked durably in its session box (seq ${seq}) and will be delivered when it next runs — subject to the deployment's mailbox retirement policy, which does not notify you. Continue with your task; do not resend the same content.${subscriptionNote}${rowNote}`
|
|
357
|
+
: `Message queued for ${peerLabel} (seq ${seq}): it is parked durably in that session's box and will be read at its next turn boundary. This receipt confirms acceptance into the box, not the peer's judgment of the message. Continue with your task; do not resend the same content.${subscriptionNote}${rowNote}`,
|
|
358
|
+
details: { type: "send-message", status: offline ? "peer_parked_offline" : "peer_queued", to, address: handle, seq, verdict, ...(subscription !== undefined ? { subscription } : {}), ...(subscriptionSeq !== undefined ? { subscriptionSeq } : {}), ...(rowRestored !== "present" ? { directoryRow: rowRestored } : {}) },
|
|
303
359
|
};
|
|
304
360
|
};
|
|
305
361
|
const settlePeerResolution = async (r) => {
|
|
@@ -329,6 +385,16 @@ export function createSendMessageTool(opts) {
|
|
|
329
385
|
}
|
|
330
386
|
}
|
|
331
387
|
};
|
|
388
|
+
if (notifyWhenIdle) {
|
|
389
|
+
if (!peerLane.active) {
|
|
390
|
+
return peerRefusal("peer_send.invalid_target", peerLane.reason !== undefined ? `notify_when_idle is only supported for peer sessions of this engine, and the cross-session lane is unavailable on this mount — ${peerLane.reason}. Nothing was sent.` : `notify_when_idle is only supported for peer sessions of this engine, and cross-session addressing is not mounted here (no peer-session directory). Nothing was sent.`);
|
|
391
|
+
}
|
|
392
|
+
if (senderIsChild) {
|
|
393
|
+
return peerRefusal("peer_send.invalid_target", "notify_when_idle is only available from the main conversation of this session (not from a subagent or teammate). Nothing was sent.");
|
|
394
|
+
}
|
|
395
|
+
const settled = await settlePeerResolution(await resolvePeerTarget());
|
|
396
|
+
return settled ?? peerRefusal("peer_send.invalid_target", `notify_when_idle is only supported for peer sessions of this engine (not teammates, subagents or agents), and no peer session is registered as "${inlineUntrusted(to, 120)}". Nothing was sent.`);
|
|
397
|
+
}
|
|
332
398
|
if (parsePeerSessionAddress(to) !== undefined) {
|
|
333
399
|
if (!peerLane.active) {
|
|
334
400
|
return peerRefusal("peer_send.invalid_target", peerLane.reason !== undefined ? `cross-session addressing is unavailable on this mount — ${peerLane.reason}. Nothing was sent.` : `cross-session addressing is not mounted here (no peer-session directory), so "${inlineUntrusted(to, 120)}" cannot be delivered.`);
|
package/dist/agents/subagent.js
CHANGED
|
@@ -2113,6 +2113,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2113
2113
|
: ctx.parentCwd !== undefined
|
|
2114
2114
|
? { parentCwd: ctx.parentCwd }
|
|
2115
2115
|
: {}),
|
|
2116
|
+
...(reviveClaim === undefined && ctx.fileHistoryLineage !== undefined ? { fileHistoryLineage: ctx.fileHistoryLineage } : {}),
|
|
2116
2117
|
...(ctx.onSubagentSpawn ? { onSubagentSpawn: ctx.onSubagentSpawn } : {}),
|
|
2117
2118
|
};
|
|
2118
2119
|
const childThinking = def?.thinking ?? ctx.thinkingLevel;
|
|
@@ -16,6 +16,16 @@ export declare function runWithStatusSink<T>(emit: (s: BrainStatus) => void, fn:
|
|
|
16
16
|
* call site for this exact reason and states the contract ("an advisory frame must never break a settled
|
|
17
17
|
* call"); the guard belongs HERE so every call site inherits it rather than each remembering.
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* #530 — bind the ACTIVE sink now, for an emit that will happen from a timer callback later. The
|
|
21
|
+
* async-local store propagates through Node's timers, but not through every timer implementation a
|
|
22
|
+
* process may run under (fake clocks in tests replace `setTimeout` outright and run callbacks from
|
|
23
|
+
* their own stack, where `getStore()` is empty). A frame scheduled inside the sink's scope must reach
|
|
24
|
+
* that sink whichever clock fires it, so the scheduler captures the store here and the callback emits
|
|
25
|
+
* through the bound function. Same swallow guard and the same retryAtMs stamp as {@link emitBrainStatus};
|
|
26
|
+
* a no-op function when no sink is active.
|
|
27
|
+
*/
|
|
28
|
+
export declare function bindBrainStatusEmitter(): (status: BrainStatus) => void;
|
|
19
29
|
export declare function emitBrainStatus(status: BrainStatus): void;
|
|
20
30
|
/**
|
|
21
31
|
* Silent-fallback telemetry (C1/C4/C5/C6) — the brain→runner TELEMETRY channel, a
|
|
@@ -3,16 +3,25 @@ const statusSinkStore = new AsyncLocalStorage();
|
|
|
3
3
|
export function runWithStatusSink(emit, fn) {
|
|
4
4
|
return statusSinkStore.run({ emit }, fn);
|
|
5
5
|
}
|
|
6
|
-
export function
|
|
6
|
+
export function bindBrainStatusEmitter() {
|
|
7
|
+
const sink = statusSinkStore.getStore();
|
|
8
|
+
if (sink === undefined)
|
|
9
|
+
return () => undefined;
|
|
10
|
+
return (status) => deliverBrainStatus(sink, status);
|
|
11
|
+
}
|
|
12
|
+
function deliverBrainStatus(sink, status) {
|
|
7
13
|
try {
|
|
8
|
-
const sink = statusSinkStore.getStore();
|
|
9
|
-
if (sink === undefined)
|
|
10
|
-
return;
|
|
11
14
|
sink.emit(withRetryDeadline(status));
|
|
12
15
|
}
|
|
13
16
|
catch {
|
|
14
17
|
}
|
|
15
18
|
}
|
|
19
|
+
export function emitBrainStatus(status) {
|
|
20
|
+
const sink = statusSinkStore.getStore();
|
|
21
|
+
if (sink === undefined)
|
|
22
|
+
return;
|
|
23
|
+
deliverBrainStatus(sink, status);
|
|
24
|
+
}
|
|
16
25
|
function withRetryDeadline(status) {
|
|
17
26
|
if (status.retryAtMs !== undefined)
|
|
18
27
|
return status;
|
|
@@ -89,6 +89,17 @@ export declare function sameRequestModuloCap(original: SSERequest, rebuilt: SSER
|
|
|
89
89
|
* Tolerates a `doFetch` (BYOM injection point) whose Response-shaped return has no usable `headers`.
|
|
90
90
|
*/
|
|
91
91
|
export declare function shouldRetryHeaderVerdict(res: Response | undefined): boolean | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* #530 — the first-token WAIT disclosure. While a first-token watchdog is armed
|
|
94
|
+
* (`firstTokenTimeoutMs > 0`) and no content token has arrived, the engine says so on the status
|
|
95
|
+
* channel: the FIRST `waiting_first_token` frame goes out once the wait has lasted
|
|
96
|
+
* {@link WAITING_FIRST_TOKEN_AFTER_MS}, and one more every {@link WAITING_FIRST_TOKEN_EVERY_MS} after
|
|
97
|
+
* that, until the first token lands, the watchdog fires, or the attempt ends. A short wait (under the
|
|
98
|
+
* threshold) announces nothing — the frame exists for the unattended "no answer for a minute" case,
|
|
99
|
+
* not for every request. The same 30s slice the retry countdown re-announces on.
|
|
100
|
+
*/
|
|
101
|
+
export declare const WAITING_FIRST_TOKEN_AFTER_MS = 30000;
|
|
102
|
+
export declare const WAITING_FIRST_TOKEN_EVERY_MS = 30000;
|
|
92
103
|
export interface SSERequest {
|
|
93
104
|
url: string;
|
|
94
105
|
headers: Record<string, string>;
|
|
@@ -3,7 +3,7 @@ import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } fr
|
|
|
3
3
|
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
|
|
4
4
|
import { classifyInputTooLong } from "./input-too-long.js";
|
|
5
5
|
import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
6
|
-
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
6
|
+
import { bindBrainStatusEmitter, emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
7
7
|
import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
|
|
8
8
|
const DEFAULT_MAX_RETRIES = 10;
|
|
9
9
|
const MAX_RETRIES_ENV_CEILING = 15;
|
|
@@ -189,6 +189,8 @@ export function shouldRetryHeaderVerdict(res) {
|
|
|
189
189
|
return undefined;
|
|
190
190
|
}
|
|
191
191
|
const RETRY_STATUS_SLICE_MS = 30_000;
|
|
192
|
+
export const WAITING_FIRST_TOKEN_AFTER_MS = 30_000;
|
|
193
|
+
export const WAITING_FIRST_TOKEN_EVERY_MS = 30_000;
|
|
192
194
|
const THINKING_RETRY_BUDGET = 2;
|
|
193
195
|
const EMPTY_USAGE = {
|
|
194
196
|
input: 0,
|
|
@@ -249,6 +251,8 @@ export function runStreamingBrain(args) {
|
|
|
249
251
|
const out = createAssistantMessageEventStream();
|
|
250
252
|
let cleanup;
|
|
251
253
|
let announcedRetry = false;
|
|
254
|
+
let announcedWait = false;
|
|
255
|
+
let terminalWaitDetail = "first token arrived";
|
|
252
256
|
let terminalRetryPhase = "recovered";
|
|
253
257
|
let terminalRetryDetail = "recovered after retrying";
|
|
254
258
|
let requestIdSeen;
|
|
@@ -257,6 +261,7 @@ export function runStreamingBrain(args) {
|
|
|
257
261
|
const aborted = signal?.aborted === true || isAbortError(err);
|
|
258
262
|
terminalRetryPhase = "gave_up";
|
|
259
263
|
terminalRetryDetail = aborted ? "cancelled while retrying" : "retries exhausted";
|
|
264
|
+
terminalWaitDetail = aborted ? "cancelled while waiting for the first token" : "the first-token wait ended in failure";
|
|
260
265
|
const errorMsg = emptyAssistant(model);
|
|
261
266
|
errorMsg.stopReason = aborted ? "aborted" : "error";
|
|
262
267
|
errorMsg.errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -278,10 +283,10 @@ export function runStreamingBrain(args) {
|
|
|
278
283
|
})
|
|
279
284
|
.finally(() => {
|
|
280
285
|
cleanup?.();
|
|
281
|
-
if (!announcedRetry)
|
|
286
|
+
if (!announcedRetry && !announcedWait)
|
|
282
287
|
return;
|
|
283
288
|
try {
|
|
284
|
-
emitBrainStatus({ phase: terminalRetryPhase, detail: terminalRetryDetail });
|
|
289
|
+
emitBrainStatus({ phase: terminalRetryPhase, detail: announcedRetry ? terminalRetryDetail : terminalWaitDetail });
|
|
285
290
|
}
|
|
286
291
|
catch {
|
|
287
292
|
}
|
|
@@ -468,11 +473,39 @@ export function runStreamingBrain(args) {
|
|
|
468
473
|
let firstTokenSeen = false;
|
|
469
474
|
let firstTokenTimedOut = false;
|
|
470
475
|
let ftTimer;
|
|
476
|
+
let waitTimer;
|
|
477
|
+
const clearWaitTimer = () => {
|
|
478
|
+
if (waitTimer) {
|
|
479
|
+
clearTimeout(waitTimer);
|
|
480
|
+
waitTimer = undefined;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
471
483
|
if (firstTokenTimeoutMs && firstTokenTimeoutMs > 0) {
|
|
472
484
|
ftTimer = setTimeout(() => {
|
|
473
485
|
firstTokenTimedOut = true;
|
|
486
|
+
clearWaitTimer();
|
|
474
487
|
void reader.cancel().catch(() => undefined);
|
|
475
488
|
}, firstTokenTimeoutMs);
|
|
489
|
+
if (firstTokenTimeoutMs > WAITING_FIRST_TOKEN_AFTER_MS) {
|
|
490
|
+
const waitStartedAt = Date.now();
|
|
491
|
+
const emitWaitFrame = bindBrainStatusEmitter();
|
|
492
|
+
const scheduleWaitFrame = (delayMs) => {
|
|
493
|
+
waitTimer = setTimeout(() => {
|
|
494
|
+
waitTimer = undefined;
|
|
495
|
+
if (firstTokenSeen || firstTokenTimedOut || Date.now() - waitStartedAt >= firstTokenTimeoutMs)
|
|
496
|
+
return;
|
|
497
|
+
announcedWait = true;
|
|
498
|
+
emitWaitFrame({
|
|
499
|
+
phase: "waiting_first_token",
|
|
500
|
+
detail: "waiting for the first token",
|
|
501
|
+
elapsedMs: Date.now() - waitStartedAt,
|
|
502
|
+
timeoutMs: firstTokenTimeoutMs,
|
|
503
|
+
});
|
|
504
|
+
scheduleWaitFrame(WAITING_FIRST_TOKEN_EVERY_MS);
|
|
505
|
+
}, delayMs);
|
|
506
|
+
};
|
|
507
|
+
scheduleWaitFrame(WAITING_FIRST_TOKEN_AFTER_MS);
|
|
508
|
+
}
|
|
476
509
|
}
|
|
477
510
|
let idleTimedOut = false;
|
|
478
511
|
let idleTimer;
|
|
@@ -497,12 +530,14 @@ export function runStreamingBrain(args) {
|
|
|
497
530
|
clearTimeout(ftTimer);
|
|
498
531
|
ftTimer = undefined;
|
|
499
532
|
}
|
|
533
|
+
clearWaitTimer();
|
|
500
534
|
};
|
|
501
535
|
cleanup = () => {
|
|
502
536
|
if (ftTimer) {
|
|
503
537
|
clearTimeout(ftTimer);
|
|
504
538
|
ftTimer = undefined;
|
|
505
539
|
}
|
|
540
|
+
clearWaitTimer();
|
|
506
541
|
if (idleTimer) {
|
|
507
542
|
clearTimeout(idleTimer);
|
|
508
543
|
idleTimer = undefined;
|
|
@@ -575,6 +610,7 @@ export function runStreamingBrain(args) {
|
|
|
575
610
|
clearTimeout(ftTimer);
|
|
576
611
|
ftTimer = undefined;
|
|
577
612
|
}
|
|
613
|
+
clearWaitTimer();
|
|
578
614
|
if (idleTimer) {
|
|
579
615
|
clearTimeout(idleTimer);
|
|
580
616
|
idleTimer = undefined;
|
|
@@ -71,9 +71,19 @@ export interface RedactionPass {
|
|
|
71
71
|
/** A literal replacement (only `$1` substitution is used by these passes) or a replacer fn. */
|
|
72
72
|
replace: string | ((match: string, ...groups: string[]) => string);
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
/**
|
|
75
|
+
* Run the declared passes in order, collecting findings (original-input spans) when a report is given.
|
|
76
|
+
*
|
|
77
|
+
* FORMAT CHARACTERS (#532): the passes scan a VIEW of the input with every `\p{Cf}` removed, so a
|
|
78
|
+
* credential split by a zero-width character is one token to the scanner, not two fragments of which
|
|
79
|
+
* only the first (or neither) matches. The OUTPUT keeps every format character that sits OUTSIDE a
|
|
80
|
+
* replaced span — a joiner in an emoji sequence, a non-joiner in Persian text, a soft hyphen in a word
|
|
81
|
+
* — and drops only those INSIDE the span the marker replaces (they were part of the secret). A
|
|
82
|
+
* format-free input takes the exact historical path (byte-identical output). When the replacements
|
|
83
|
+
* cannot be mapped back exactly (a later pass matched across an earlier pass's inserted text), the
|
|
84
|
+
* output is the redacted view itself — every format character dropped: the failure direction is
|
|
85
|
+
* over-stripping, never a leak.
|
|
86
|
+
*/
|
|
77
87
|
export declare function runRedactionPasses(input: string, passes: readonly RedactionPass[], report?: RedactionReport): string;
|
|
78
88
|
/** The credential-shape passes (the pre-channel SECRET_PATTERNS, now with declared kinds/confidence).
|
|
79
89
|
* Exported for {@link runRedactionPasses} composition (untrusted-egress chains its URL passes after
|
package/dist/core/arg-summary.js
CHANGED
|
@@ -25,10 +25,10 @@ function mapBackOnePass(edits, pos) {
|
|
|
25
25
|
return pos - delta;
|
|
26
26
|
}
|
|
27
27
|
const PREEXISTING_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const FORMAT_CHAR_RE = /\p{Cf}/u;
|
|
29
|
+
const MAX_FORMAT_SLOT_SCANS = 64;
|
|
30
|
+
const REDACTED_WHOLE = "[redacted]";
|
|
31
|
+
function applyPasses(input, passes, report, toOriginal) {
|
|
32
32
|
const batches = [];
|
|
33
33
|
let cur = input;
|
|
34
34
|
for (const pass of passes) {
|
|
@@ -51,18 +51,149 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
51
51
|
report.findings.push({
|
|
52
52
|
kind: pass.kind,
|
|
53
53
|
confidence: pass.confidence,
|
|
54
|
-
span: [s0, e0],
|
|
54
|
+
span: [toOriginal(s0), toOriginal(e0)],
|
|
55
55
|
marker: pass.marker,
|
|
56
56
|
source: pass.source,
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length });
|
|
59
|
+
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length, inserted });
|
|
60
60
|
}
|
|
61
61
|
return inserted;
|
|
62
62
|
});
|
|
63
63
|
batches.push(edits);
|
|
64
64
|
}
|
|
65
|
-
return cur;
|
|
65
|
+
return { out: cur, batches };
|
|
66
|
+
}
|
|
67
|
+
function replacedSpans(batches) {
|
|
68
|
+
const spans = [];
|
|
69
|
+
for (let j = 0; j < batches.length; j++) {
|
|
70
|
+
for (const e of batches[j]) {
|
|
71
|
+
let s0 = e.at;
|
|
72
|
+
let e0 = e.at + e.removedLen;
|
|
73
|
+
for (let i = j - 1; i >= 0; i--) {
|
|
74
|
+
s0 = mapBackOnePass(batches[i], s0);
|
|
75
|
+
e0 = mapBackOnePass(batches[i], e0);
|
|
76
|
+
}
|
|
77
|
+
if (e0 - s0 !== e.removedLen)
|
|
78
|
+
return undefined;
|
|
79
|
+
spans.push({ start: s0, end: e0, inserted: e.inserted, pass: j });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
spans.sort((a, b) => a.start - b.start);
|
|
83
|
+
for (let i = 1; i < spans.length; i++)
|
|
84
|
+
if (spans[i].start < spans[i - 1].end)
|
|
85
|
+
return undefined;
|
|
86
|
+
return spans;
|
|
87
|
+
}
|
|
88
|
+
export function runRedactionPasses(input, passes, report) {
|
|
89
|
+
if (report !== undefined && report.preexistingMarkers === undefined) {
|
|
90
|
+
report.preexistingMarkers = input.match(PREEXISTING_MARKER_RE)?.length ?? 0;
|
|
91
|
+
}
|
|
92
|
+
if (!FORMAT_CHAR_RE.test(input))
|
|
93
|
+
return applyPasses(input, passes, report, (pos) => pos).out;
|
|
94
|
+
const viewChars = [];
|
|
95
|
+
const viewToOriginal = [];
|
|
96
|
+
const formatAtSlot = [];
|
|
97
|
+
const charStartOffsets = [];
|
|
98
|
+
{
|
|
99
|
+
let orig = 0;
|
|
100
|
+
let viewOff = 0;
|
|
101
|
+
let slotBuf = "";
|
|
102
|
+
for (const ch of input) {
|
|
103
|
+
if (FORMAT_CHAR_RE.test(ch)) {
|
|
104
|
+
slotBuf += ch;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
formatAtSlot.push(slotBuf);
|
|
108
|
+
slotBuf = "";
|
|
109
|
+
viewToOriginal.push(orig);
|
|
110
|
+
charStartOffsets.push(viewOff);
|
|
111
|
+
viewChars.push(ch);
|
|
112
|
+
viewOff += ch.length;
|
|
113
|
+
}
|
|
114
|
+
orig += ch.length;
|
|
115
|
+
}
|
|
116
|
+
formatAtSlot.push(slotBuf);
|
|
117
|
+
viewToOriginal.push(input.length);
|
|
118
|
+
charStartOffsets.push(viewOff);
|
|
119
|
+
}
|
|
120
|
+
const view = viewChars.join("");
|
|
121
|
+
const offsetToCharIndex = new Map();
|
|
122
|
+
charStartOffsets.forEach((off, idx) => offsetToCharIndex.set(off, idx));
|
|
123
|
+
const toOriginal = (pos) => {
|
|
124
|
+
const idx = offsetToCharIndex.get(pos);
|
|
125
|
+
return idx === undefined ? input.length : viewToOriginal[idx];
|
|
126
|
+
};
|
|
127
|
+
const { out, batches } = applyPasses(view, passes, report, toOriginal);
|
|
128
|
+
const viewSpans = replacedSpans(batches);
|
|
129
|
+
if (viewSpans === undefined)
|
|
130
|
+
return out;
|
|
131
|
+
const spans = viewSpans.map((sp) => {
|
|
132
|
+
const startIdx = offsetToCharIndex.get(sp.start) ?? viewChars.length;
|
|
133
|
+
const endIdx = offsetToCharIndex.get(sp.end) ?? viewChars.length;
|
|
134
|
+
const lastIdx = endIdx - 1;
|
|
135
|
+
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
136
|
+
return { start: viewToOriginal[startIdx], end: endOrig, inserted: sp.inserted };
|
|
137
|
+
});
|
|
138
|
+
const viewOffsetToOriginal = (viewStart, viewEnd) => {
|
|
139
|
+
const startIdx = offsetToCharIndex.get(viewStart) ?? viewChars.length;
|
|
140
|
+
const endIdx = offsetToCharIndex.get(viewEnd) ?? viewChars.length;
|
|
141
|
+
const lastIdx = endIdx - 1;
|
|
142
|
+
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
143
|
+
return { start: viewToOriginal[startIdx], end: endOrig };
|
|
144
|
+
};
|
|
145
|
+
const cutOffsets = [];
|
|
146
|
+
for (let k = 1; k < viewChars.length; k++)
|
|
147
|
+
if (formatAtSlot[k] !== "")
|
|
148
|
+
cutOffsets.push(charStartOffsets[k]);
|
|
149
|
+
if (cutOffsets.length > MAX_FORMAT_SLOT_SCANS)
|
|
150
|
+
return REDACTED_WHOLE;
|
|
151
|
+
const extra = [];
|
|
152
|
+
const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos).batches);
|
|
153
|
+
if (rawSpans !== undefined)
|
|
154
|
+
extra.push(...rawSpans);
|
|
155
|
+
for (const cut of cutOffsets) {
|
|
156
|
+
const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos).batches);
|
|
157
|
+
if (cutSpans === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
for (const c of cutSpans) {
|
|
160
|
+
const o = viewOffsetToOriginal(c.start + cut, c.end + cut);
|
|
161
|
+
extra.push({ start: o.start, end: o.end, inserted: c.inserted, pass: c.pass });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
extra.sort((a, b) => a.start - b.start);
|
|
165
|
+
for (const r of extra) {
|
|
166
|
+
const overlapping = spans.find((v) => r.start < v.end && v.start < r.end);
|
|
167
|
+
if (overlapping !== undefined) {
|
|
168
|
+
if (r.start < overlapping.start)
|
|
169
|
+
overlapping.start = r.start;
|
|
170
|
+
if (r.end > overlapping.end)
|
|
171
|
+
overlapping.end = r.end;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
spans.push({ start: r.start, end: r.end, inserted: r.inserted });
|
|
175
|
+
if (report !== undefined) {
|
|
176
|
+
const pass = passes[r.pass];
|
|
177
|
+
report.findings.push({ kind: pass.kind, confidence: pass.confidence, span: [r.start, r.end], marker: pass.marker, source: pass.source });
|
|
178
|
+
}
|
|
179
|
+
spans.sort((a, b) => a.start - b.start);
|
|
180
|
+
}
|
|
181
|
+
for (let i = 1; i < spans.length;) {
|
|
182
|
+
if (spans[i].start < spans[i - 1].end) {
|
|
183
|
+
spans[i - 1].end = Math.max(spans[i - 1].end, spans[i].end);
|
|
184
|
+
spans.splice(i, 1);
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
let result = "";
|
|
190
|
+
let cursor = 0;
|
|
191
|
+
for (const sp of spans) {
|
|
192
|
+
result += input.slice(cursor, sp.start) + sp.inserted;
|
|
193
|
+
cursor = sp.end;
|
|
194
|
+
}
|
|
195
|
+
result += input.slice(cursor);
|
|
196
|
+
return result;
|
|
66
197
|
}
|
|
67
198
|
export const SECRET_PASSES = [
|
|
68
199
|
{ 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]" },
|
|
@@ -11,3 +11,14 @@ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2000;
|
|
|
11
11
|
* recipe canonicalizer needs its VALUE and must not load the assembly face (and its SHA-locked assets)
|
|
12
12
|
* to get it; `auto-mode-prompt.js` re-exports it, so the public name and its module face are unchanged. */
|
|
13
13
|
export declare const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
|
|
14
|
+
/** CC 2.1.250 `FO = {maxConsecutive: 3, maxTotal: 20}` — the classifier DENIAL LIMITS applied when
|
|
15
|
+
* `RunnerDeps.autoMode.denialLimit` omits a bound. Count-then-judge: the block that reaches a bound is
|
|
16
|
+
* itself the one that falls back to a person (the 3rd consecutive block asks; the 20th total asks). */
|
|
17
|
+
export declare const AUTO_MODE_DENIAL_LIMIT_DEFAULTS: Readonly<{
|
|
18
|
+
maxConsecutive: number;
|
|
19
|
+
maxTotal: number;
|
|
20
|
+
}>;
|
|
21
|
+
/** CC 2.1.250 `AKe = 120000` — the fallback ask's auto-deny window (ms) when `autoDenyAfterMs` is omitted.
|
|
22
|
+
* `0` disarms the window (the ask waits on the approver alone — the upstream original "fall back to
|
|
23
|
+
* prompting" shape). */
|
|
24
|
+
export declare const AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS = 120000;
|
|
@@ -3,3 +3,5 @@ export const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
|
|
|
3
3
|
export const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
|
|
4
4
|
export const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2_000;
|
|
5
5
|
export const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
|
|
6
|
+
export const AUTO_MODE_DENIAL_LIMIT_DEFAULTS = Object.freeze({ maxConsecutive: 3, maxTotal: 20 });
|
|
7
|
+
export const AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS = 120_000;
|