@sema-agent/core 2.11.0 → 2.13.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.
Files changed (37) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +1 -0
  2. package/dist/agents/agent-transcript-tool.js +1 -1
  3. package/dist/brain/stream-engine.js +4 -1
  4. package/dist/core/auto-promote.js +2 -1
  5. package/dist/core/checkpoint-store.d.ts +1 -0
  6. package/dist/core/checkpoint-store.js +3 -0
  7. package/dist/core/git-worktree-env.js +5 -0
  8. package/dist/core/mcp.d.ts +3 -0
  9. package/dist/core/mcp.js +91 -7
  10. package/dist/core/remote-env.d.ts +3 -0
  11. package/dist/core/remote-env.js +19 -1
  12. package/dist/core/runner/active-skill-scope.js +34 -6
  13. package/dist/core/runner/assemble-result.d.ts +3 -0
  14. package/dist/core/runner/assemble-result.js +4 -1
  15. package/dist/core/runner/prepare-task.d.ts +4 -0
  16. package/dist/core/runner/prepare-task.js +120 -25
  17. package/dist/core/runner/runtask.js +49 -10
  18. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  19. package/dist/core/runner/tool-disclosure.js +26 -7
  20. package/dist/core/session-store.js +15 -4
  21. package/dist/core/skill-tool-specifier.d.ts +8 -0
  22. package/dist/core/skill-tool-specifier.js +58 -0
  23. package/dist/core/skills-directory.d.ts +1 -1
  24. package/dist/core/skills-directory.js +16 -4
  25. package/dist/core/types.d.ts +11 -5
  26. package/dist/core/with-retry.js +0 -1
  27. package/dist/engine/harness/types.d.ts +1 -1
  28. package/dist/index.d.ts +3 -3
  29. package/dist/index.js +1 -1
  30. package/dist/internal/llm.d.ts +1 -1
  31. package/dist/orchestration/workflow.js +8 -4
  32. package/dist/tools/fs/bash-readonly-classifier.js +38 -6
  33. package/dist/tools/fs/fs-bash.js +14 -0
  34. package/dist/tools/scheduler-tools.js +38 -18
  35. package/dist/tools/web.d.ts +8 -2
  36. package/dist/tools/web.js +46 -17
  37. package/package.json +1 -1
@@ -0,0 +1,58 @@
1
+ import { parsePermissionRule, wildcardMatch } from "./permission-rules.js";
2
+ import { canonicalToolName } from "./tool-name-aliases.js";
3
+ import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
4
+ import { parseLeadingCommandName } from "../tools/fs/bash-readonly-classifier.js";
5
+ const SPECIFIER_ENFORCED_TOOLS = new Set(COARSE_SHELL_TOOLS.map(canonicalToolName));
6
+ export function parseSkillToolEntry(entry) {
7
+ const parsed = parsePermissionRule(entry);
8
+ const name = canonicalToolName(parsed.toolName);
9
+ return parsed.ruleContent === undefined ? { raw: entry, name } : { raw: entry, name, specifier: parsed.ruleContent };
10
+ }
11
+ export function isSkillSpecifierEnforced(canonicalName) {
12
+ return SPECIFIER_ENFORCED_TOOLS.has(canonicalName);
13
+ }
14
+ function normalizeSpacing(s) {
15
+ return s.trim().replace(/[ \t]+/g, " ");
16
+ }
17
+ function hasUnescapedStar(content) {
18
+ for (let i = 0; i < content.length; i++) {
19
+ if (content[i] !== "*")
20
+ continue;
21
+ let backslashes = 0;
22
+ for (let j = i - 1; j >= 0 && content[j] === "\\"; j--)
23
+ backslashes++;
24
+ if (backslashes % 2 === 0)
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ function specifierMatchesCommand(specifier, command) {
30
+ const spec = normalizeSpacing(specifier);
31
+ const prefix = /^(.+):\*$/.exec(spec)?.[1];
32
+ if (prefix !== undefined) {
33
+ return command === prefix || command.startsWith(prefix + " ");
34
+ }
35
+ if (hasUnescapedStar(spec))
36
+ return wildcardMatch(spec, command);
37
+ return command === spec;
38
+ }
39
+ export function skillSpecifierRejection(entry, args) {
40
+ const { specifier } = entry;
41
+ if (specifier === undefined)
42
+ return undefined;
43
+ if (!isSkillSpecifierEnforced(entry.name)) {
44
+ return `entry "${entry.raw}" narrows a tool whose calls this gate cannot match against a command pattern — only the shell tools (${[...SPECIFIER_ENFORCED_TOOLS].join(", ")}) carry a command string, so the entry admits nothing`;
45
+ }
46
+ const command = args?.command;
47
+ if (typeof command !== "string") {
48
+ return `entry "${entry.raw}" requires a command string to match against, and this call has none`;
49
+ }
50
+ const parsedCommand = parseLeadingCommandName(command);
51
+ if ("reject" in parsedCommand) {
52
+ return `entry "${entry.raw}" only admits a single simple command (${parsedCommand.reject})`;
53
+ }
54
+ if (!specifierMatchesCommand(specifier, normalizeSpacing(command))) {
55
+ return `command "${command.trim()}" is not admitted by entry "${entry.raw}"`;
56
+ }
57
+ return undefined;
58
+ }
@@ -1,5 +1,5 @@
1
1
  import type { SkillSpec } from "./types.js";
2
- export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
2
+ export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "allowed_tool_pattern_unsupported" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
3
3
  export interface SkillsDirectoryWarning {
4
4
  code: SkillsDirectoryWarningCode;
5
5
  skill: string;
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
2
  import { isAbsolute, join, relative } from "node:path";
3
3
  import { canonicalToolName } from "./tool-name-aliases.js";
4
+ import { isSkillSpecifierEnforced, parseSkillToolEntry } from "./skill-tool-specifier.js";
4
5
  const SKILL_FILE = "SKILL.md";
5
6
  const RESOURCE_DIRS = ["assets", "references", "scripts"];
6
7
  const NAME_MAX_CHARS = 64;
@@ -316,12 +317,23 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
316
317
  const names = [];
317
318
  const seen = new Set();
318
319
  for (const n of splitToolNames(declared)) {
319
- const key = canonicalToolName(n);
320
+ const entry = parseSkillToolEntry(n);
321
+ const key = entry.specifier === undefined ? entry.name : `${entry.name}(${entry.specifier})`;
320
322
  if (seen.has(key))
321
323
  continue;
322
324
  seen.add(key);
323
325
  names.push(n);
324
326
  }
327
+ for (const n of names) {
328
+ const entry = parseSkillToolEntry(n);
329
+ if (entry.specifier !== undefined && !isSkillSpecifierEnforced(entry.name)) {
330
+ warn({
331
+ code: "allowed_tool_pattern_unsupported",
332
+ skill: skillName,
333
+ detail: `allowed-tools entry "${n}" narrows a tool whose calls cannot be matched against a command pattern — only shell tools carry one, so the entry is not enforced and the tool is DISABLED inside this skill frame. Use the bare tool name to allow it fully.`,
334
+ });
335
+ }
336
+ }
325
337
  if (names.length === 0)
326
338
  return undefined;
327
339
  if (names.some((n) => n === ALL_TOOLS_WILDCARD))
@@ -329,9 +341,9 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
329
341
  let allowTools = names;
330
342
  if (deployedTools !== undefined) {
331
343
  const mounted = new Set(deployedTools.map(canonicalToolName));
332
- allowTools = names.filter((n) => mounted.has(canonicalToolName(n)));
344
+ allowTools = names.filter((n) => mounted.has(parseSkillToolEntry(n).name));
333
345
  for (const n of names) {
334
- if (!mounted.has(canonicalToolName(n))) {
346
+ if (!mounted.has(parseSkillToolEntry(n).name)) {
335
347
  warn({
336
348
  code: "allowed_tool_not_mounted",
337
349
  skill: skillName,
@@ -342,7 +354,7 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
342
354
  }
343
355
  if (disallowed.length > 0) {
344
356
  const denied = new Set(disallowed.map(canonicalToolName));
345
- allowTools = allowTools.filter((n) => !denied.has(canonicalToolName(n)));
357
+ allowTools = allowTools.filter((n) => !denied.has(parseSkillToolEntry(n).name));
346
358
  }
347
359
  return { allowTools, lineageId: `skill:${skillName}` };
348
360
  }
@@ -1,6 +1,6 @@
1
1
  import type { TSchema } from "typebox";
2
2
  import type { AgentTool, ThinkingLevel } from "../internal/harness.js";
3
- import type { DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
3
+ import type { CompleteSimpleFn, DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
4
4
  import type { TaskNotificationPayload } from "./task-notification.js";
5
5
  export type ModelRef = string | Model;
6
6
  export type ModelRole = "default" | "summarize" | "subagent" | "team" | "synthesize" | "advisor" | "verifier" | "classifier";
@@ -13,10 +13,7 @@ export type RoleSpec = ModelRef | {
13
13
  export type ModelRoles = Partial<Record<ModelRole, RoleSpec>>;
14
14
  export interface Brain {
15
15
  stream: StreamFn;
16
- complete?: (model: Model, context: {
17
- systemPrompt?: string;
18
- messages: unknown[];
19
- }, options?: unknown) => Promise<unknown>;
16
+ complete?: CompleteSimpleFn;
20
17
  }
21
18
  export type ToolEffect = "read" | "write" | "idempotent";
22
19
  export interface ToolSpec<TParams extends TSchema = TSchema> {
@@ -370,6 +367,13 @@ export interface TaskSpec {
370
367
  signal?: AbortSignal;
371
368
  }
372
369
  export type TaskStatus = "completed" | "blocked" | "failed" | "timeout" | "suspended" | "needs_review";
370
+ export interface RemoteEnvFailureNote {
371
+ op: "suspendVM" | "resumeVM" | "postResumeInit";
372
+ code: import("./remote-env.js").RemoteExecutionErrorCode;
373
+ retryable: boolean;
374
+ attempts: number;
375
+ message: string;
376
+ }
373
377
  export interface TaskResult {
374
378
  taskId: string;
375
379
  sessionId: string;
@@ -380,6 +384,8 @@ export interface TaskResult {
380
384
  blockedReason?: string;
381
385
  checkpointToken?: import("./checkpoint-store.js").CheckpointToken;
382
386
  checkpointGate?: import("./checkpoint-store.js").CheckpointGate;
387
+ workspaceRestoreMode?: "snapshot" | "park_only";
388
+ remoteEnvFailures?: RemoteEnvFailureNote[];
383
389
  errorMessage?: string;
384
390
  errorCode?: string;
385
391
  degraded?: {
@@ -13,7 +13,6 @@ export async function withRetry(op, policy, opts = {}) {
13
13
  opts.signal?.removeEventListener("abort", onAbort);
14
14
  resolve();
15
15
  }, ms);
16
- timer.unref?.();
17
16
  opts.signal?.addEventListener("abort", onAbort, { once: true });
18
17
  }));
19
18
  let last = await op(1);
@@ -47,7 +47,7 @@ export declare class FileError extends Error {
47
47
  path?: string;
48
48
  constructor(code: FileErrorCode, message: string, path?: string, cause?: Error);
49
49
  }
50
- export type ExecutionErrorCode = "aborted" | "timeout" | "shell_unavailable" | "spawn_error" | "callback_error" | "transport_lost" | "unknown";
50
+ export type ExecutionErrorCode = "aborted" | "timeout" | "shell_unavailable" | "spawn_error" | "callback_error" | "transport_lost" | "suspended" | "auth_failed" | "unknown";
51
51
  export declare class ExecutionError extends Error {
52
52
  code: ExecutionErrorCode;
53
53
  partialStdout?: string;
package/dist/index.d.ts CHANGED
@@ -56,7 +56,7 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
56
56
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
57
57
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
58
58
  export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode } from "./internal/harness.js";
59
- export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, } from "./core/remote-env.js";
59
+ export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
60
60
  export { withRetry } from "./core/with-retry.js";
61
61
  export type { RetryPolicy, RetryResult } from "./core/with-retry.js";
62
62
  export type { RemoteExecutionEnv, WorkspaceHandle, SnapshotId, SessionToken, SandboxTier, OutputChunk, ExecStreamOptions, RemoteConnectConfig, VmLifecycleOptions, SecretRef, RemoteExecutionErrorCode, ExecutionEnvFactory, ExecutionEnvFactoryContext, } from "./core/remote-env.js";
@@ -188,8 +188,8 @@ export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type D
188
188
  export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
189
189
  export { type BrainTimeoutConfig } from "./brain/timeout.js";
190
190
  export { createAssistantMessageEventStream } from "./internal/llm.js";
191
- export type { AssistantMessage, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
192
- export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
191
+ export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
192
+ export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
193
193
  export { Type } from "typebox";
194
194
  export type { TSchema, Static } from "typebox";
195
195
  export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
51
51
  export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
52
52
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
53
53
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
54
- export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, } from "./core/remote-env.js";
54
+ export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
55
55
  export { withRetry } from "./core/with-retry.js";
56
56
  export { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "./core/git-worktree-env.js";
57
57
  export { runExecGate } from "./core/exec-gate.js";
@@ -1,2 +1,2 @@
1
1
  export { createAssistantMessageEventStream, stripEngineMetadata } from "../engine/llm/index.js";
2
- export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
2
+ export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
@@ -480,13 +480,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
480
480
  if (row !== undefined)
481
481
  row.sessionId = sessionId;
482
482
  };
483
- const bceTick = (callKey, e) => {
483
+ const bceTick = (callKey, e, fromSessionId) => {
484
484
  if (!bceSink)
485
485
  return;
486
486
  const id = waIdOf(callKey);
487
487
  const row = bceLive.get(id);
488
488
  if (row === undefined)
489
489
  return;
490
+ if (fromSessionId !== undefined && row.sessionId !== undefined && fromSessionId !== row.sessionId)
491
+ return;
490
492
  bceEmit({
491
493
  kind: "tick",
492
494
  taskId: id,
@@ -507,8 +509,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
507
509
  if (!bceSink)
508
510
  return;
509
511
  const id = waIdOf(callKey);
512
+ const row = bceLive.get(id);
510
513
  if (!bceLive.delete(id))
511
514
  return;
515
+ const coord = sessionId ?? row?.sessionId;
512
516
  bceEmit({
513
517
  kind: "terminal",
514
518
  taskId: id,
@@ -516,7 +520,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
516
520
  owner: runId,
517
521
  workflowRunId: runId,
518
522
  ...(scope !== undefined ? { scope } : {}),
519
- ...(sessionId !== undefined ? { sessionId, transcriptId: sessionId } : {}),
523
+ ...(coord !== undefined ? { sessionId: coord, transcriptId: coord } : {}),
520
524
  status,
521
525
  summary: boundedRedactedSummary(summary, 300),
522
526
  ...(stats !== undefined ? { usage: { tokens: stats.tokens, turns: stats.turns, ...(stats.costMicroUsd !== undefined ? { costMicroUsd: stats.costMicroUsd } : {}) } } : {}),
@@ -873,7 +877,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
873
877
  ? {
874
878
  onForwardEvent: (e) => {
875
879
  if (e.type === "task_progress")
876
- bceTick(callKey, e);
880
+ bceTick(callKey, e, attemptSessionId);
877
881
  try {
878
882
  baseInternals.onForwardEvent?.(e);
879
883
  }
@@ -1181,7 +1185,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1181
1185
  ? {
1182
1186
  onForwardEvent: (e) => {
1183
1187
  if (e.type === "task_progress")
1184
- bceTick(callKey, e);
1188
+ bceTick(callKey, e, childSessionId);
1185
1189
  try {
1186
1190
  enrichedForwardS?.(e);
1187
1191
  }
@@ -107,10 +107,42 @@ function hasUnquotedExpansionMetachar(rawToken) {
107
107
  }
108
108
  return false;
109
109
  }
110
+ function longOptionNameOf(tok) {
111
+ if (!tok.startsWith("--") || tok.length === 2)
112
+ return undefined;
113
+ const body = tok.slice(2);
114
+ const eq = body.indexOf("=");
115
+ const name = eq >= 0 ? body.slice(0, eq) : body;
116
+ return name.length > 0 ? name : undefined;
117
+ }
118
+ function isLongOptionAbbrevOf(name, full) {
119
+ return full.startsWith(name);
120
+ }
110
121
  function isGrepPatternFlagToken(tok) {
111
- if (tok.startsWith("--"))
112
- return tok.startsWith("--regexp") || tok.startsWith("--file");
113
- return /^-[A-Za-z]*[ef]/.test(tok);
122
+ if (tok.startsWith("--")) {
123
+ const long = longOptionNameOf(tok);
124
+ return long !== undefined && (isLongOptionAbbrevOf(long, "regexp") || isLongOptionAbbrevOf(long, "file"));
125
+ }
126
+ return /^-[A-Za-z0-9]*[ef]/.test(tok);
127
+ }
128
+ function isGrepPatternPayloadLongOption(tok) {
129
+ if (!tok.includes("="))
130
+ return false;
131
+ const name = longOptionNameOf(tok);
132
+ return name !== undefined && isLongOptionAbbrevOf(name, "regexp");
133
+ }
134
+ function isCutDelimiterPayloadLongOption(tok) {
135
+ if (!tok.includes("="))
136
+ return false;
137
+ const name = longOptionNameOf(tok);
138
+ return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
139
+ }
140
+ function isGrepFileStdinLongOption(tok) {
141
+ const eq = tok.indexOf("=");
142
+ if (eq < 0 || tok.slice(eq + 1) !== "-")
143
+ return false;
144
+ const name = longOptionNameOf(tok);
145
+ return name !== undefined && isLongOptionAbbrevOf(name, "file");
114
146
  }
115
147
  function grepClusterValueOwner(tok) {
116
148
  if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
@@ -176,9 +208,9 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
176
208
  k++;
177
209
  continue;
178
210
  }
179
- if (name === "cut" && (/^-d./.test(t) || /^--(output-)?delimiter=/.test(t)))
211
+ if (name === "cut" && (/^-d./.test(t) || isCutDelimiterPayloadLongOption(t)))
180
212
  continue;
181
- if (name === "grep" && (grepClusterValueOwner(t) === "e" || /^--regexp=/.test(t)))
213
+ if (name === "grep" && (grepClusterValueOwner(t) === "e" || isGrepPatternPayloadLongOption(t)))
182
214
  continue;
183
215
  if (t.startsWith("-") && t !== "-") {
184
216
  for (const payload of attachedOptionPayloads(t)) {
@@ -312,7 +344,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
312
344
  k++;
313
345
  continue;
314
346
  }
315
- if (name === "grep" && (t === "-f-" || t === "--file=-")) {
347
+ if (name === "grep" && (t === "-f-" || isGrepFileStdinLongOption(t))) {
316
348
  hasStdinDash = true;
317
349
  break;
318
350
  }
@@ -229,6 +229,20 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
229
229
  isError: true,
230
230
  };
231
231
  }
232
+ if (res.error.code === "suspended") {
233
+ return {
234
+ content: `Error (${toolName}): the execution environment is suspended (its workspace VM is paused), so the command did NOT run and no command can run in this leg. Do not retry it here — the task must be resumed first; report that the workspace is suspended. (${res.error.message})`,
235
+ details: { type: "bash", envSuspended: true },
236
+ isError: true,
237
+ };
238
+ }
239
+ if (res.error.code === "auth_failed") {
240
+ return {
241
+ content: `Error (${toolName}): authentication to the execution environment was rejected, so the command did NOT run. Do NOT retry — this is a permanent credential failure that an operator must fix; report it instead of trying other commands. (${res.error.message})`,
242
+ details: { type: "bash", authFailed: true },
243
+ isError: true,
244
+ };
245
+ }
232
246
  if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
233
247
  const rawStdout = res.error.partialStdout ?? "";
234
248
  const rawStderr = res.error.partialStderr ?? "";
@@ -83,16 +83,13 @@ function expandCronField(field, { min, max }) {
83
83
  return out.size > 0 ? [...out].sort((a, b) => a - b) : null;
84
84
  }
85
85
  export function cronScheduleError(expr) {
86
- const parts = expr.trim().split(/\s+/);
87
- let std = parts;
88
- if (parts.length === 6) {
89
- if (!expandCronField(parts[0] ?? "", { min: 0, max: 59 })) {
90
- return `invalid seconds field "${parts[0]}" — allowed 0-59 (syntax: N, N-M, */N, comma lists).`;
91
- }
92
- std = parts.slice(1);
86
+ const std = expr.trim().split(/\s+/);
87
+ if (std.length === 6) {
88
+ return (`its leading seconds field is not supported — the resident scheduler fires on 5-field cron, so one ` +
89
+ `minute is the finest granularity available. Pass "${std.slice(1).join(" ")}" if that cadence works.`);
93
90
  }
94
- else if (parts.length !== 5) {
95
- return `expected 5 fields (minute hour day-of-month month day-of-week) or 6 with leading seconds, got ${parts.length}.`;
91
+ if (std.length !== 5) {
92
+ return `expected 5 fields (minute hour day-of-month month day-of-week), got ${std.length}.`;
96
93
  }
97
94
  const expanded = [];
98
95
  for (let i = 0; i < 5; i++) {
@@ -117,9 +114,12 @@ export function cronScheduleError(expr) {
117
114
  return null;
118
115
  }
119
116
  const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
117
+ function cronExprFromSummary(when) {
118
+ const trimmed = when.trim();
119
+ return trimmed.startsWith("cron ") ? trimmed.slice("cron ".length).trim() : trimmed;
120
+ }
120
121
  export function cronToHuman(expr) {
121
- const parts = expr.trim().split(/\s+/);
122
- const std = parts.length === 6 ? parts.slice(1) : parts;
122
+ const std = cronExprFromSummary(expr).split(/\s+/);
123
123
  if (std.length !== 5)
124
124
  return expr;
125
125
  const [minute = "", hour = "", dom = "", month = "", dow = ""] = std;
@@ -158,6 +158,12 @@ export function createSchedulerTools(env, ctx) {
158
158
  return [];
159
159
  const sched = env;
160
160
  const schedCtx = toSchedulerContext(ctx);
161
+ let cronCreateChain = Promise.resolve();
162
+ const serializedCronCreateOp = (fn) => {
163
+ const next = cronCreateChain.then(fn, fn);
164
+ cronCreateChain = next.then(() => undefined, () => undefined);
165
+ return next;
166
+ };
161
167
  const cronCreateDescription = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders. The task runs UNATTENDED when it fires, so write a fully self-contained \`prompt\` (it will not see this conversation). Use CronList to see what you've scheduled, CronDelete to remove one.
162
168
 
163
169
  ## One-shot tasks (schedule kind "delay" or "at")
@@ -191,7 +197,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
191
197
  parameters: Type.Object({
192
198
  prompt: Type.String({ description: "Self-contained instructions for the future task (it runs unattended)." }),
193
199
  schedule: Type.Optional(Type.Union([
194
- Type.Object({ kind: Type.Literal("cron"), expr: Type.String({ description: "5- or 6-field cron expression." }) }),
200
+ Type.Object({
201
+ kind: Type.Literal("cron"),
202
+ expr: Type.String({ description: "5-field cron expression (minute hour day-of-month month day-of-week)." }),
203
+ }),
195
204
  Type.Object({ kind: Type.Literal("at"), atMs: Type.Number({ description: "Absolute epoch ms to fire once." }) }),
196
205
  Type.Object({ kind: Type.Literal("delay"), delaySec: Type.Number({ description: "Seconds from now to fire once." }) }),
197
206
  ], { description: 'When to fire. Pass exactly one of `schedule` or `cron`. Defaults to `durable: true`.' })),
@@ -207,7 +216,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
207
216
  label: Type.Optional(Type.String({ description: "Short label (also a dedup key with the schedule)." })),
208
217
  }),
209
218
  effect: "write",
210
- execute: async (args) => {
219
+ execute: async (args) => serializedCronCreateOp(async () => {
211
220
  const a = args;
212
221
  if (a.schedule !== undefined && a.cron !== undefined) {
213
222
  return errorResult("Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.");
@@ -221,7 +230,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
221
230
  const when = a.schedule ?? { kind: "cron", expr: a.cron };
222
231
  if (when.kind === "cron") {
223
232
  if (!isValidCronExpr(when.expr)) {
224
- return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed.`);
233
+ return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5 space-separated cron fields (digits and * / , -) are allowed (a 6th leading seconds field is parsed but always rejected next — this scheduler has no seconds hand).`);
225
234
  }
226
235
  const deepErr = cronScheduleError(when.expr);
227
236
  if (deepErr)
@@ -246,25 +255,35 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
246
255
  ...(!durable ? { lifetime: "session" } : {}),
247
256
  ...(a.recurring !== undefined ? { recurring: a.recurring } : {}),
248
257
  };
258
+ const before = await sched.list(schedCtx);
259
+ const knownIds = before.ok ? new Set(before.value.map((s) => s.id)) : undefined;
249
260
  const r = await sched.schedule(intent, schedCtx);
250
261
  if (!r.ok)
251
262
  return errorResult(`Error (CronCreate): ${r.error.message}`);
263
+ const replaced = knownIds?.has(r.value.id);
252
264
  const humanSchedule = when.kind === "cron"
253
265
  ? cronToHuman(when.expr)
254
266
  : when.kind === "delay"
255
267
  ? `once in ${when.delaySec}s`
256
268
  : `once at ${new Date(when.atMs).toISOString()}`;
269
+ const named = `${r.value.id}${a.label ? ` (${a.label})` : ""}`;
270
+ const content = replaced === true
271
+ ? `Updated scheduled task ${named} — it replaced an existing job with the same schedule and label, whose prompt is now gone.`
272
+ : replaced === false
273
+ ? `Scheduled task ${named}.`
274
+ : `Scheduled (or updated) task ${named} — the scheduler's listing was unavailable, so whether this replaced an existing job with the same schedule and label is unknown.`;
257
275
  return {
258
- content: `Scheduled task ${r.value.id}${a.label ? ` (${a.label})` : ""}.`,
276
+ content,
259
277
  details: {
260
278
  type: "cron-create",
261
279
  id: r.value.id,
262
280
  humanSchedule,
263
281
  recurring: when.kind === "cron" && a.recurring !== false,
264
282
  durable,
283
+ ...(replaced !== undefined ? { replaced } : {}),
265
284
  },
266
285
  };
267
- },
286
+ }),
268
287
  });
269
288
  const cronCancel = defineTool({
270
289
  name: "CronDelete",
@@ -298,17 +317,18 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
298
317
  }
299
318
  const content = r.value
300
319
  .map((s) => {
320
+ const shown = cronExprFromSummary(s.when);
301
321
  const human = cronToHuman(s.when);
302
322
  const tier = s.lifetime === "session" ? " [session]" : "";
303
323
  const once = s.recurring === false ? " [once]" : "";
304
- return `- ${s.id}: ${s.when}${human !== s.when ? ` — ${human}` : ""}${s.label ? ` (${s.label})` : ""}${tier}${once}`;
324
+ return `- ${s.id}: ${shown}${human !== s.when ? ` — ${human}` : ""}${s.label ? ` (${s.label})` : ""}${tier}${once}`;
305
325
  })
306
326
  .join("\n");
307
327
  const jobs = r.value.map((s) => {
308
328
  const human = cronToHuman(s.when);
309
329
  return {
310
330
  id: s.id,
311
- cron: s.when,
331
+ cron: cronExprFromSummary(s.when),
312
332
  humanSchedule: human !== s.when ? human : s.when,
313
333
  ...(s.label !== undefined ? { label: s.label } : {}),
314
334
  ...(s.recurring !== undefined ? { recurring: s.recurring } : {}),
@@ -6,7 +6,10 @@ export interface WebFetchConfig {
6
6
  fetchImpl?: typeof fetch;
7
7
  maxBytes?: number;
8
8
  timeoutMs?: number;
9
- summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string>;
9
+ summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
10
+ text: string;
11
+ truncated?: boolean;
12
+ }>;
10
13
  userAgent?: string;
11
14
  }
12
15
  export declare function htmlToText(html: string): string;
@@ -14,7 +17,10 @@ export declare function webFetchToolSpec(config?: WebFetchConfig): ToolSpec;
14
17
  export declare function createWebFetchTool(config?: WebFetchConfig): AgentTool;
15
18
  export declare const WEBFETCH_SUMMARY_MAX_CONTENT = 100000;
16
19
  export declare const WEBFETCH_SUMMARY_GUIDELINES: string;
17
- export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string>;
20
+ export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
21
+ text: string;
22
+ truncated?: boolean;
23
+ }>;
18
24
  export interface WebSearchConfig {
19
25
  search: (query: string, signal?: AbortSignal, opts?: {
20
26
  allowedDomains?: string[];