@agent-compose/sdk 0.8.3 → 0.8.4

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.
@@ -80,13 +80,17 @@ export type AgentMessageSummary = {
80
80
  * an agent.message event (the terminating `text` carries the whole block) —
81
81
  * the loop filters it before summarizing. `task_notification` is filtered
82
82
  * too: it is session-transport metadata (a parent harness's background-task
83
- * completion echo), not the agent's own output. */
83
+ * completion echo), not the agent's own output. `harness_notice` likewise:
84
+ * harness-composed advisory text (synthetic assistant messages), never the
85
+ * agent speaking. */
84
86
  type DurableAgentMessage = Exclude<AgentMessage, {
85
87
  type: "text_delta";
86
88
  } | {
87
89
  type: "usage_delta";
88
90
  } | {
89
91
  type: "task_notification";
92
+ } | {
93
+ type: "harness_notice";
90
94
  }>;
91
95
  export declare function summarizeAgentMessage(msg: DurableAgentMessage): AgentMessageSummary;
92
96
  export type AgentLifecycleEvent = {
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export { RequestContext, ReservedKeyError, NonSerialisableValueError, AC_RESERVE
24
24
  export type { RequestContextReserved, RequestContextWire, } from "./request-context/index.js";
25
25
  export { Verdict, runProcessorChain, denyTools, humanApproval, requireScope, redactPattern, createGatePauseProcessor, createAskHumanProcessor, ASK_USER_QUESTION_TOOL, } from "./processors/index.js";
26
26
  export type { Processor, ProcessorContext, ProcessorVerdict, ToolCall, GatePausePolicy, GatePauseApproval, GatePauseConnection, } from "./processors/index.js";
27
- export type { AgentMessage, AgentMessageInit, AgentMessageText, AgentMessageThinking, AgentMessageToolUse, AgentMessageToolResult, AgentMessageDone, AgentMessageError, AgentMessageUsage, AgentMessageTaskNotification, AgentStatus, } from "./types/protocol.js";
27
+ export type { AgentMessage, AgentMessageInit, AgentMessageText, AgentMessageThinking, AgentMessageToolUse, AgentMessageToolResult, AgentMessageDone, AgentMessageError, AgentMessageUsage, AgentMessageTaskNotification, AgentMessageHarnessNotice, AgentStatus, } from "./types/protocol.js";
28
28
  export type { SandboxProvider, DesktopSandboxProvider, SandboxPtyOpts, SandboxPtyHandle, } from "./types/sandbox.js";
29
29
  export { AgentComposeClient } from "./client.js";
30
30
  export type { RegisterResult, RegisterWorkflowInput, RuntimeSourceInput, TemplateSourceRef, InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, FundingChoice, InlineWorkflowPayload, InvokeInlineOptions, InvokeInlineAndWaitOptions, ListSnapshotsOptions, TemplateRow, ListTemplatesOptions, CreateFactoryInput, UpdateFactoryInput, SecretOptions, SetSecretResult, SecretListEntry, SessionSecretEntry, SessionSecretInput, SessionSecretRequestCreated, SessionSecretRequestStatus, CreateApiKeyInput, StreamRunLogsOptions, TeamMember, Mention, CreateMentionsInput, EventSubjectType, EventRow, ReportEventInput, ListEventsOptions, ListEventsResult, RunLogLine, ListRunLogsOptions, RunListEntry, ListRunsOptions, RunDetail, FundingLane, RunFundingStamp, RunFundingUsageRow, RunFundingResponse, RunStepUsage, RunStepUsageResponse, RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage, UsageRollupRow, UsageResponse, CancelRunResponse, RequestAgentPauseOptions, RequestAgentPauseResponse, SendAgentMessageOptions, SendAgentMessageResponse, AnswerSteerOptions, ResumePauseOptions, ResumePauseResponse, ResumePauseSuccess, ResumePausePending, ResumePauseActor, ConversationMessagePart, ConversationRow, ConversationMessageRow, ConversationsPage, ConversationDetail, ConversationThread, ConversationPageContext, SendConversationMessageInput, SendConversationMessageResult, UnnotifiedMention, ConversationTurnState, ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow, CreateCloudSessionInput, CloudSessionCreated, SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld, SessionFileChange, SessionChangeSet, SessionChangeStats, SessionDiffReviewBound, ReviewSuggestion, ReviewSuggestionInput, ReviewNotesPublished, ReviewGitCredential, SessionReviewDocument, ReviewSuggestionDecision, ReviewSuggestionDecisions, SessionMergeReportDetail, SessionMergeReport, SessionMergeGated, SessionDiscardReport, SessionPreflightFileRow, SessionChangePreflight, SessionRebaseReport, DriveMountSession, CreateDriveMountSessionInput, ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted, SessionDirectMessageSent, BranchClaimInfo, BranchClaimGranted, BranchClaimsReleased, BranchClaimState, SessionPerfBucket, SessionPerfHistoryPage, FactoryPerfSummary, FactoryFileSearchRow, FactoryFolderSearchRow, SearchFactoryFilesOptions, FactoryFileSearchResult, PublicFileLinkState, FactoryFileListRow, ListFactoryFilesOptions, FactoryFileListPage, ConversationMemberRole, ConversationMember, DocumentCapability, TemplateCapability, ScopeGrant, ArtifactScope, RunContext, SetScopeGrantsInput, SetTemplateScopeInput, TemplateDetail, Project, ProjectsPage, ProjectRole, ProjectMember, ProjectObject, ProjectObjectsPage, ProjectSkippedFile, ProjectPreviewFile, ProjectAddPreview, AddProjectObjectInput, AddProjectObjectResult, RefreshProjectObjectResult, ComplianceScopeKind, ComplianceStatus, ComplianceSession, ComplianceAccess, RequestComplianceSessionInput, ListComplianceSessionsOptions, ComplianceSessionsPage, ListComplianceAccessesOptions, ComplianceAccessesPage, } from "./client.js";
@@ -51,7 +51,7 @@ export { createCodexRuntime, codexSpec } from "./runtimes/codex.js";
51
51
  export type { CodexRuntimeConfig, CodexReasoningEffort } from "./runtimes/codex.js";
52
52
  export { default as codexRuntime } from "./runtimes/codex.js";
53
53
  export type { CliReasoningEffort } from "./runtimes/_cli-agent.js";
54
- export { shellQuote, uniquePromptPath, turnLivenessProbeCommand, parseTurnProbeOutput, livenessVerdictFromReading, runnerBusySentinelFragment, exitEventPushCommand, injectAppendAndAckCommand, INJECT_ACK_EXEC_TIMEOUT_MS, streamInputFeederFragment, sessionEnvSourceFragment, credEnvSourceFragment, residentFeederFragment, residentIdleCheckFragment, residentIdleWatchdogFragment, residentResultCountCommand, deferredExitEventPushCommand, RESIDENT_IDLE_TTL_S, RESIDENT_WATCHDOG_POLL_S, RESIDENT_RESULT_LINE_PREFIX, SESSION_ENV_FILE_RELPATH, LAUNCH_EXEC_TIMEOUT_MS, TURN_PROBE_TIMEOUT_MS, INSTALL_PROBE_TIMEOUT_MS, CLI_INSTALL_TIMEOUT_MS, RUNNER_HEARTBEAT_INTERVAL_SECONDS, RUNNER_HEARTBEAT_STALE_MS, } from "./runtimes/_cli-agent.js";
54
+ export { shellQuote, uniquePromptPath, turnLivenessProbeCommand, parseTurnProbeOutput, livenessVerdictFromReading, runnerBusySentinelFragment, exitEventPushCommand, injectAppendAndAckCommand, INJECT_ACK_EXEC_TIMEOUT_MS, wrapMidTurnUserMessage, streamInputFeederFragment, sessionEnvSourceFragment, credEnvSourceFragment, residentFeederFragment, residentIdleCheckFragment, residentIdleWatchdogFragment, residentResultCountCommand, deferredExitEventPushCommand, RESIDENT_IDLE_TTL_S, RESIDENT_WATCHDOG_POLL_S, RESIDENT_RESULT_LINE_PREFIX, SESSION_ENV_FILE_RELPATH, LAUNCH_EXEC_TIMEOUT_MS, TURN_PROBE_TIMEOUT_MS, INSTALL_PROBE_TIMEOUT_MS, CLI_INSTALL_TIMEOUT_MS, RUNNER_HEARTBEAT_INTERVAL_SECONDS, RUNNER_HEARTBEAT_STALE_MS, } from "./runtimes/_cli-agent.js";
55
55
  export type { TurnProbeReading, CliAgentSpec } from "./runtimes/_cli-agent.js";
56
56
  export { perfSamplerFunctionFragment, standalonePerfProbeCommand, parsePerfToken, parsePerfProbeOutput, PERF_SAMPLE_EVERY_BEATS, PERF_PROBE_WINDOW_SECONDS, PERF_TOKEN_MAX_CHARS, } from "./agent/perf-sampler.js";
57
57
  export type { GuestPerfSample } from "./agent/perf-sampler.js";
@@ -73,7 +73,7 @@ export { default as claudeCodeRuntime } from "./runtimes/claude-code.js";
73
73
  export { bashTool, codingTools, editTool, readTool, writeTool } from "./tools/index.js";
74
74
  export type { CodingTool } from "./tools/index.js";
75
75
  export type { RunEvent } from "./types/events.js";
76
- export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById, getSandboxQuotas, listOwnedSandboxes, deleteSandboxSnapshot, snapshotResolves, makeSandboxProvider, makeDesktopSandboxProvider, parseSseExecStream, AGENT_COMPOSE_TAG, SANDBOX_SIZES, SANDBOX_MACHINES, SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, SESSION_DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES, E2B_MAX_VCPUS, E2B_MAX_MEMORY_MB, VERCEL_MEMORY_MB_PER_VCPU, isE2bSupportedSize, isVercelSupportedSize, sandboxSizeLabel, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate, isPlatformE2bTemplateAlias, E2B_DEVBOX_TEMPLATE, E2B_DEVBOX_SPEC, E2B_DEVBOX_RECIPE_VERSION, e2bDevboxTemplateRef } from "./sandbox.js";
76
+ export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById, getSandboxQuotas, listOwnedSandboxes, deleteSandboxSnapshot, snapshotResolves, makeSandboxProvider, makeDesktopSandboxProvider, parseSseExecStream, AGENT_COMPOSE_TAG, SANDBOX_SIZES, SANDBOX_MACHINES, SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, SESSION_DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES, E2B_MAX_VCPUS, E2B_MAX_MEMORY_MB, VERCEL_MEMORY_MB_PER_VCPU, isE2bSupportedSize, isVercelSupportedSize, sandboxSizeLabel, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate, e2bMaxSandboxMs, isPlatformE2bTemplateAlias, E2B_DEVBOX_TEMPLATE, E2B_DEVBOX_SPEC, E2B_DEVBOX_RECIPE_VERSION, e2bDevboxTemplateRef } from "./sandbox.js";
77
77
  export { SandboxUnavailableError, SANDBOX_UNAVAILABLE_PREFIX } from "./sandbox-errors.js";
78
78
  export type { SandboxCreateOpts, SandboxNetworkPolicy, SandboxNetworkHeaderTransform, SandboxNetworkAllowRule, SandboxNetworkSubnetPolicy, SandboxProviderName, SandboxQuotaResult, OwnedSandboxResult, OwnedSandbox, SandboxSize, ParseSseExecStreamOptions, SandboxCommandRunOptions, SandboxCommandResult, } from "./sandbox.js";
79
79
  export type { SandboxResources, DriveMergePolicy } from "./types/workflow-metadata.js";
package/dist/index.js CHANGED
@@ -3413,7 +3413,7 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
3413
3413
  continue;
3414
3414
  }
3415
3415
  const msg = outputVerdict.value;
3416
- if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification")
3416
+ if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification" || msg.type === "harness_notice")
3417
3417
  continue;
3418
3418
  opts.onAgentEvent?.(iteration, msg);
3419
3419
  const summary = summarizeAgentMessage(msg);
@@ -4038,6 +4038,13 @@ function streamInputFeederFragment(paths) {
4038
4038
  const q2 = shellQuote;
4039
4039
  return `rm -f ${q2(paths.fifoPath)}; mkfifo ${q2(paths.fifoPath)}; ` + `: > ${q2(paths.inboxPath)}; echo 0 > ${q2(paths.deliveredPath)}; ` + `( cat ${q2(paths.promptPath)}; ac_fed=0; while kill -0 "$$" 2>/dev/null; do ` + `if tail -c ${INJECT_FEEDER_RESULT_TAIL_BYTES} ${q2(paths.outPath)} 2>/dev/null | grep -q '"type":"result"'; then break; fi; ` + `ac_lines=$(wc -l < ${q2(paths.inboxPath)} 2>/dev/null || echo 0); ac_lines=\${ac_lines:-0}; ` + `if [ "$ac_lines" -gt "$ac_fed" ]; then ` + `sed -n "$((ac_fed+1)),$((ac_lines))p" ${q2(paths.inboxPath)}; ac_fed=$ac_lines; ` + `echo "$ac_fed" > ${q2(paths.deliveredPath)}; fi; ` + `sleep ${INJECT_FEEDER_POLL_SECONDS}; done ) > ${q2(paths.fifoPath)} 2>/dev/null </dev/null & `;
4040
4040
  }
4041
+ function wrapMidTurnUserMessage(text) {
4042
+ return `The user sent a new message while you were working:
4043
+
4044
+ ` + text + `
4045
+
4046
+ Address the message above as you continue this turn.`;
4047
+ }
4041
4048
  function injectAppendAndAckCommand(args) {
4042
4049
  const q2 = shellQuote;
4043
4050
  return `printf '%s\\n' ${q2(args.line)} >> ${q2(args.inboxPath)}; i=0; ` + `while [ "$i" -lt ${INJECT_ACK_POLL_ATTEMPTS} ]; do ` + `d=$(cat ${q2(args.deliveredPath)} 2>/dev/null || echo 0); ` + `[ "\${d:-0}" -ge ${args.target} ] && exit 0; ` + `kill -0 ${args.pid} 2>/dev/null || exit 4; ` + `sleep ${INJECT_ACK_POLL_SECONDS}; i=$((i+1)); done; exit 5`;
@@ -4172,7 +4179,7 @@ class CliAgentRunner {
4172
4179
  const streamInput = this.spec.streamInput;
4173
4180
  if (!inject || !streamInput || this.guestDetached)
4174
4181
  return "unsupported";
4175
- const line = streamInput.messageLine(text);
4182
+ const line = streamInput.messageLine(wrapMidTurnUserMessage(text));
4176
4183
  const attempt = inject.chain.then(async () => {
4177
4184
  if (this.turnInject !== inject)
4178
4185
  return "closed";
@@ -5115,10 +5122,11 @@ var claudeCodeSpec = {
5115
5122
  switch (p.type) {
5116
5123
  case "assistant": {
5117
5124
  const message = p.message;
5125
+ const synthetic = message?.model === "<synthetic>";
5118
5126
  const blocks2 = Array.isArray(message?.content) ? message.content : [];
5119
5127
  return blocks2.flatMap((b) => {
5120
5128
  if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
5121
- return [{ type: "text", text: b.text, timestamp: ts }];
5129
+ return [{ type: synthetic ? "harness_notice" : "text", text: b.text, timestamp: ts }];
5122
5130
  }
5123
5131
  if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
5124
5132
  return [{ type: "thinking", text: b.thinking, timestamp: ts }];
@@ -5434,6 +5442,7 @@ function makeSandboxProvider(sb) {
5434
5442
  function e2bMaxSandboxMs() {
5435
5443
  return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
5436
5444
  }
5445
+ var e2bExtendClampWarned = false;
5437
5446
  function wrapE2bBackgroundProcess(handle) {
5438
5447
  return {
5439
5448
  pid: handle.pid,
@@ -5527,7 +5536,12 @@ function makeE2bSandboxProvider(sb) {
5527
5536
  return { resumeHandle: sb.sandboxId };
5528
5537
  },
5529
5538
  async extendLifetime(ms) {
5530
- await sb.setTimeout(Math.min(ms, e2bMaxSandboxMs()));
5539
+ const maxMs = e2bMaxSandboxMs();
5540
+ if (ms > maxMs && !e2bExtendClampWarned) {
5541
+ e2bExtendClampWarned = true;
5542
+ console.warn(`[sandbox] e2b lifetime extension clamped: requested ${ms}ms exceeds the plan cap ${maxMs}ms — ` + `the kill-clock backstop fires at the cap, not the requested horizon. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
5543
+ }
5544
+ await sb.setTimeout(Math.min(ms, maxMs));
5531
5545
  },
5532
5546
  async updateNetworkPolicy(policy) {
5533
5547
  await sb.updateNetwork(toE2bNetwork(policy));
@@ -5559,6 +5573,7 @@ var e2bProviderDef = {
5559
5573
  const sandboxOpts = {
5560
5574
  ...rest,
5561
5575
  timeoutMs: Math.min(timeoutMs, maxMs),
5576
+ lifecycle: { onTimeout: "pause", autoResume: true },
5562
5577
  ...networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}
5563
5578
  };
5564
5579
  const resolvedSize = size ?? DEFAULT_SANDBOX_SIZE;
@@ -8976,6 +8991,7 @@ function createDisplayPromoter(ctx = {}) {
8976
8991
  export {
8977
8992
  writeTool,
8978
8993
  writeAgentContext,
8994
+ wrapMidTurnUserMessage,
8979
8995
  uniquePromptPath,
8980
8996
  turnLivenessProbeCommand,
8981
8997
  streamInputFeederFragment,
@@ -9053,6 +9069,7 @@ export {
9053
9069
  explainBundleFailure,
9054
9070
  exitEventPushCommand,
9055
9071
  editTool,
9072
+ e2bMaxSandboxMs,
9056
9073
  e2bMachineSpec,
9057
9074
  e2bDevboxTemplateRef,
9058
9075
  e2bBaseTemplate,
@@ -149,6 +149,18 @@ export declare function streamInputFeederFragment(paths: {
149
149
  deliveredPath: string;
150
150
  outPath: string;
151
151
  }): string;
152
+ /** The instructional envelope around a user message delivered INTO a
153
+ * running turn. Claude Code's own interactive harness wraps queued
154
+ * mid-turn input with an explicit "address this as you continue"
155
+ * instruction; a bare `--input-format stream-json` line carries no such
156
+ * framing, so an injected message reads like any other transcript text
157
+ * and the model tends to continue its work narration without
158
+ * acknowledging it. Every mid-turn inject producer (the runner's
159
+ * `injectUserMessage` and the workflow lane's `deliverInjectedMessage`
160
+ * activity) MUST wrap through here — the wrapper rides only the wire to
161
+ * the model; the stored conversation row keeps the user's raw text.
162
+ * The text is pinned by tests: change it deliberately or not at all. */
163
+ export declare function wrapMidTurnUserMessage(text: string): string;
152
164
  /** The one delivery-ack exec `injectUserMessage` runs: append the message
153
165
  * line to the durable inbox, then wait for the feeder's delivered counter
154
166
  * to cover it. Exit 0 = delivered into the CLI's stdin pre-result; 4 = the
@@ -3413,7 +3413,7 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
3413
3413
  continue;
3414
3414
  }
3415
3415
  const msg = outputVerdict.value;
3416
- if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification")
3416
+ if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification" || msg.type === "harness_notice")
3417
3417
  continue;
3418
3418
  opts.onAgentEvent?.(iteration, msg);
3419
3419
  const summary = summarizeAgentMessage(msg);
@@ -4038,6 +4038,13 @@ function streamInputFeederFragment(paths) {
4038
4038
  const q2 = shellQuote;
4039
4039
  return `rm -f ${q2(paths.fifoPath)}; mkfifo ${q2(paths.fifoPath)}; ` + `: > ${q2(paths.inboxPath)}; echo 0 > ${q2(paths.deliveredPath)}; ` + `( cat ${q2(paths.promptPath)}; ac_fed=0; while kill -0 "$$" 2>/dev/null; do ` + `if tail -c ${INJECT_FEEDER_RESULT_TAIL_BYTES} ${q2(paths.outPath)} 2>/dev/null | grep -q '"type":"result"'; then break; fi; ` + `ac_lines=$(wc -l < ${q2(paths.inboxPath)} 2>/dev/null || echo 0); ac_lines=\${ac_lines:-0}; ` + `if [ "$ac_lines" -gt "$ac_fed" ]; then ` + `sed -n "$((ac_fed+1)),$((ac_lines))p" ${q2(paths.inboxPath)}; ac_fed=$ac_lines; ` + `echo "$ac_fed" > ${q2(paths.deliveredPath)}; fi; ` + `sleep ${INJECT_FEEDER_POLL_SECONDS}; done ) > ${q2(paths.fifoPath)} 2>/dev/null </dev/null & `;
4040
4040
  }
4041
+ function wrapMidTurnUserMessage(text) {
4042
+ return `The user sent a new message while you were working:
4043
+
4044
+ ` + text + `
4045
+
4046
+ Address the message above as you continue this turn.`;
4047
+ }
4041
4048
  function injectAppendAndAckCommand(args) {
4042
4049
  const q2 = shellQuote;
4043
4050
  return `printf '%s\\n' ${q2(args.line)} >> ${q2(args.inboxPath)}; i=0; ` + `while [ "$i" -lt ${INJECT_ACK_POLL_ATTEMPTS} ]; do ` + `d=$(cat ${q2(args.deliveredPath)} 2>/dev/null || echo 0); ` + `[ "\${d:-0}" -ge ${args.target} ] && exit 0; ` + `kill -0 ${args.pid} 2>/dev/null || exit 4; ` + `sleep ${INJECT_ACK_POLL_SECONDS}; i=$((i+1)); done; exit 5`;
@@ -4172,7 +4179,7 @@ class CliAgentRunner {
4172
4179
  const streamInput = this.spec.streamInput;
4173
4180
  if (!inject || !streamInput || this.guestDetached)
4174
4181
  return "unsupported";
4175
- const line = streamInput.messageLine(text);
4182
+ const line = streamInput.messageLine(wrapMidTurnUserMessage(text));
4176
4183
  const attempt = inject.chain.then(async () => {
4177
4184
  if (this.turnInject !== inject)
4178
4185
  return "closed";
@@ -5115,10 +5122,11 @@ var claudeCodeSpec = {
5115
5122
  switch (p.type) {
5116
5123
  case "assistant": {
5117
5124
  const message = p.message;
5125
+ const synthetic = message?.model === "<synthetic>";
5118
5126
  const blocks2 = Array.isArray(message?.content) ? message.content : [];
5119
5127
  return blocks2.flatMap((b) => {
5120
5128
  if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
5121
- return [{ type: "text", text: b.text, timestamp: ts }];
5129
+ return [{ type: synthetic ? "harness_notice" : "text", text: b.text, timestamp: ts }];
5122
5130
  }
5123
5131
  if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
5124
5132
  return [{ type: "thinking", text: b.thinking, timestamp: ts }];
@@ -5434,6 +5442,7 @@ function makeSandboxProvider(sb) {
5434
5442
  function e2bMaxSandboxMs() {
5435
5443
  return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
5436
5444
  }
5445
+ var e2bExtendClampWarned = false;
5437
5446
  function wrapE2bBackgroundProcess(handle) {
5438
5447
  return {
5439
5448
  pid: handle.pid,
@@ -5527,7 +5536,12 @@ function makeE2bSandboxProvider(sb) {
5527
5536
  return { resumeHandle: sb.sandboxId };
5528
5537
  },
5529
5538
  async extendLifetime(ms) {
5530
- await sb.setTimeout(Math.min(ms, e2bMaxSandboxMs()));
5539
+ const maxMs = e2bMaxSandboxMs();
5540
+ if (ms > maxMs && !e2bExtendClampWarned) {
5541
+ e2bExtendClampWarned = true;
5542
+ console.warn(`[sandbox] e2b lifetime extension clamped: requested ${ms}ms exceeds the plan cap ${maxMs}ms — ` + `the kill-clock backstop fires at the cap, not the requested horizon. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
5543
+ }
5544
+ await sb.setTimeout(Math.min(ms, maxMs));
5531
5545
  },
5532
5546
  async updateNetworkPolicy(policy) {
5533
5547
  await sb.updateNetwork(toE2bNetwork(policy));
@@ -5559,6 +5573,7 @@ var e2bProviderDef = {
5559
5573
  const sandboxOpts = {
5560
5574
  ...rest,
5561
5575
  timeoutMs: Math.min(timeoutMs, maxMs),
5576
+ lifecycle: { onTimeout: "pause", autoResume: true },
5562
5577
  ...networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}
5563
5578
  };
5564
5579
  const resolvedSize = size ?? DEFAULT_SANDBOX_SIZE;
package/dist/sandbox.d.ts CHANGED
@@ -20,7 +20,7 @@ export { AGENT_COMPOSE_TAG } from "./sandbox/provider-def.js";
20
20
  export type { SandboxCreateOpts, OwnedSandbox } from "./sandbox/provider-def.js";
21
21
  export { parseSseExecStream } from "./sandbox/exec-stream.js";
22
22
  export type { ParseSseExecStreamOptions } from "./sandbox/exec-stream.js";
23
- export { makeSandboxProvider } from "./sandbox/providers/e2b.js";
23
+ export { makeSandboxProvider, e2bMaxSandboxMs } from "./sandbox/providers/e2b.js";
24
24
  export { makeDesktopSandboxProvider } from "./sandbox/providers/desktop.js";
25
25
  export { VERCEL_MAX_TAGS, buildVercelTags } from "./sandbox/providers/vercel.js";
26
26
  export { makeLocalSandboxProvider } from "./sandbox/providers/local.js";
@@ -144,7 +144,19 @@ export interface AgentMessageTaskNotification extends AgentMessageBase {
144
144
  durationMs?: number;
145
145
  };
146
146
  }
147
- export type AgentMessage = AgentMessageInit | AgentMessageText | AgentMessageTextDelta | AgentMessageThinking | AgentMessageToolUse | AgentMessageToolResult | AgentMessageDone | AgentMessageError | AgentMessageUsage | AgentMessageUsageDelta | AgentMessagePlan | AgentMessageTaskNotification;
147
+ /** HARNESS-authored notice text — content the CLI composed itself rather
148
+ * than the model speaking: slash-command stdout, model/skills advisories,
149
+ * queued-input notes. claude-code marks these structurally (assistant
150
+ * events whose `message.model` is `"<synthetic>"`), and the normaliser
151
+ * forwards them under this kind so downstream persists them as collapsed
152
+ * system notices instead of agent prose (the 2026-08-22 "(session model)"
153
+ * / skills-reminder leak: harness plumbing rendered as message content).
154
+ * Additive kind: existing producers never emit it. */
155
+ export interface AgentMessageHarnessNotice extends AgentMessageBase {
156
+ type: "harness_notice";
157
+ text: string;
158
+ }
159
+ export type AgentMessage = AgentMessageInit | AgentMessageText | AgentMessageTextDelta | AgentMessageThinking | AgentMessageToolUse | AgentMessageToolResult | AgentMessageDone | AgentMessageError | AgentMessageUsage | AgentMessageUsageDelta | AgentMessagePlan | AgentMessageTaskNotification | AgentMessageHarnessNotice;
148
160
  /** Status block the agent emits to signal iteration completion or blockers. */
149
161
  export interface AgentStatus {
150
162
  summary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/sdk",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -93,10 +93,13 @@ function preview(value: unknown): string {
93
93
  * an agent.message event (the terminating `text` carries the whole block) —
94
94
  * the loop filters it before summarizing. `task_notification` is filtered
95
95
  * too: it is session-transport metadata (a parent harness's background-task
96
- * completion echo), not the agent's own output. */
96
+ * completion echo), not the agent's own output. `harness_notice` likewise:
97
+ * harness-composed advisory text (synthetic assistant messages), never the
98
+ * agent speaking. */
97
99
  type DurableAgentMessage = Exclude<
98
100
  AgentMessage,
99
101
  { type: "text_delta" } | { type: "usage_delta" } | { type: "task_notification" }
102
+ | { type: "harness_notice" }
100
103
  >;
101
104
 
102
105
  export function summarizeAgentMessage(msg: DurableAgentMessage): AgentMessageSummary {
@@ -484,8 +487,10 @@ export async function agentLoop<TResponse = unknown>(opts: AgentLoopOpts<TRespon
484
487
  }
485
488
  const msg = outputVerdict.value;
486
489
  // A processor cannot re-introduce a live-only chunk; task
487
- // notifications are transport metadata, never loop output.
488
- if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification") continue;
490
+ // notifications and harness notices are transport metadata, never
491
+ // loop output.
492
+ if (msg.type === "text_delta" || msg.type === "usage_delta"
493
+ || msg.type === "task_notification" || msg.type === "harness_notice") continue;
489
494
  opts.onAgentEvent?.(iteration, msg);
490
495
  // Usage summaries carry the resolved model so the server can price
491
496
  // token rows per model without correlating back to agent.spawned.
package/src/index.ts CHANGED
@@ -101,6 +101,7 @@ export type {
101
101
  AgentMessageError,
102
102
  AgentMessageUsage,
103
103
  AgentMessageTaskNotification,
104
+ AgentMessageHarnessNotice,
104
105
  AgentStatus,
105
106
  } from "./types/protocol.js";
106
107
 
@@ -266,7 +267,7 @@ export {
266
267
  shellQuote, uniquePromptPath,
267
268
  turnLivenessProbeCommand, parseTurnProbeOutput, livenessVerdictFromReading,
268
269
  runnerBusySentinelFragment, exitEventPushCommand,
269
- injectAppendAndAckCommand, INJECT_ACK_EXEC_TIMEOUT_MS,
270
+ injectAppendAndAckCommand, INJECT_ACK_EXEC_TIMEOUT_MS, wrapMidTurnUserMessage,
270
271
  streamInputFeederFragment, sessionEnvSourceFragment, credEnvSourceFragment,
271
272
  residentFeederFragment, residentIdleCheckFragment, residentIdleWatchdogFragment,
272
273
  residentResultCountCommand, deferredExitEventPushCommand,
@@ -323,7 +324,7 @@ export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById,
323
324
  E2B_TEMPLATE_SIZES, E2B_MAX_VCPUS, E2B_MAX_MEMORY_MB,
324
325
  VERCEL_MEMORY_MB_PER_VCPU,
325
326
  isE2bSupportedSize, isVercelSupportedSize, sandboxSizeLabel,
326
- e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate,
327
+ e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate, e2bMaxSandboxMs,
327
328
  isPlatformE2bTemplateAlias,
328
329
  // ADR-0038 "Computer" — the per-member persistent desktop machine template.
329
330
  E2B_DEVBOX_TEMPLATE, E2B_DEVBOX_SPEC, E2B_DEVBOX_RECIPE_VERSION,
@@ -252,6 +252,23 @@ export function streamInputFeederFragment(paths: {
252
252
  + `sleep ${INJECT_FEEDER_POLL_SECONDS}; done ) > ${q(paths.fifoPath)} 2>/dev/null </dev/null & `;
253
253
  }
254
254
 
255
+ /** The instructional envelope around a user message delivered INTO a
256
+ * running turn. Claude Code's own interactive harness wraps queued
257
+ * mid-turn input with an explicit "address this as you continue"
258
+ * instruction; a bare `--input-format stream-json` line carries no such
259
+ * framing, so an injected message reads like any other transcript text
260
+ * and the model tends to continue its work narration without
261
+ * acknowledging it. Every mid-turn inject producer (the runner's
262
+ * `injectUserMessage` and the workflow lane's `deliverInjectedMessage`
263
+ * activity) MUST wrap through here — the wrapper rides only the wire to
264
+ * the model; the stored conversation row keeps the user's raw text.
265
+ * The text is pinned by tests: change it deliberately or not at all. */
266
+ export function wrapMidTurnUserMessage(text: string): string {
267
+ return "The user sent a new message while you were working:\n\n"
268
+ + text
269
+ + "\n\nAddress the message above as you continue this turn.";
270
+ }
271
+
255
272
  /** The one delivery-ack exec `injectUserMessage` runs: append the message
256
273
  * line to the durable inbox, then wait for the feeder's delivered counter
257
274
  * to cover it. Exit 0 = delivered into the CLI's stdin pre-result; 4 = the
@@ -823,7 +840,10 @@ export class CliAgentRunner implements ModelExecutionContract {
823
840
  const inject = this.turnInject;
824
841
  const streamInput = this.spec.streamInput;
825
842
  if (!inject || !streamInput || this.guestDetached) return "unsupported";
826
- const line = streamInput.messageLine(text);
843
+ // The address-this envelope (wrapMidTurnUserMessage): a raw injected
844
+ // line carries no framing, and the model continues its narration
845
+ // without acknowledging the message.
846
+ const line = streamInput.messageLine(wrapMidTurnUserMessage(text));
827
847
  const attempt = inject.chain.then(async (): Promise<"delivered" | "pending" | "closed"> => {
828
848
  if (this.turnInject !== inject) return "closed";
829
849
  // The append lands whether or not the ack times out, so the target
@@ -291,11 +291,20 @@ export const claudeCodeSpec: CliAgentSpec = {
291
291
  switch (p.type) {
292
292
  // Assistant API message: content blocks → text / thinking / tool_use.
293
293
  case "assistant": {
294
- const message = p.message as { content?: ClaudeContentBlock[] } | undefined;
294
+ const message = p.message as { content?: ClaudeContentBlock[]; model?: unknown } | undefined;
295
+ // HARNESS-synthesized assistant messages: the CLI stamps
296
+ // `message.model: "<synthetic>"` on content it composed itself —
297
+ // slash-command stdout, model/skills advisories — never the model
298
+ // speaking (verified live on claude 2.1.212 and 2.1.236: /model and
299
+ // /context output both arrive this way). Their text forwards as
300
+ // `harness_notice` so downstream renders it as a quiet system note
301
+ // instead of agent prose (the 2026-08-22 plumbing-as-content leak).
302
+ // Structural marker only — no content sniffing.
303
+ const synthetic = message?.model === "<synthetic>";
295
304
  const blocks = Array.isArray(message?.content) ? message.content : [];
296
305
  return blocks.flatMap((b): AgentMessage[] => {
297
306
  if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
298
- return [{ type: "text", text: b.text, timestamp: ts }];
307
+ return [{ type: synthetic ? "harness_notice" : "text", text: b.text, timestamp: ts }];
299
308
  }
300
309
  if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
301
310
  return [{ type: "thinking", text: b.thinking, timestamp: ts }];
@@ -104,6 +104,9 @@ export function e2bMaxSandboxMs(): number {
104
104
  return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
105
105
  }
106
106
 
107
+ /** Once-per-process latch for the extendLifetime clamp warning below. */
108
+ let e2bExtendClampWarned = false;
109
+
107
110
  /** Wrap an E2B `CommandHandle` as a provider-agnostic background process.
108
111
  * `wait()` is normalised NOT to throw on a non-zero exit (mirroring the
109
112
  * `commands.run` contract in `makeSandboxProvider`) so callers branch on
@@ -245,7 +248,20 @@ function makeE2bSandboxProvider(sb: Sandbox): SandboxProvider {
245
248
  // the create timeout — an over-cap push would 400 and leave the OLD
246
249
  // (possibly short) deadline standing.
247
250
  async extendLifetime(ms) {
248
- await sb.setTimeout(Math.min(ms, e2bMaxSandboxMs()));
251
+ const maxMs = e2bMaxSandboxMs();
252
+ if (ms > maxMs && !e2bExtendClampWarned) {
253
+ // Once per process, not per extension: every far-horizon push clamps
254
+ // identically, and the fact worth surfacing is the CONFIGURATION —
255
+ // the server believes it bought `ms` of kill-clock slack but the
256
+ // plan cap silently shortens it, so the "orphan backstop" can reap
257
+ // a machine the platform still considers covered.
258
+ e2bExtendClampWarned = true;
259
+ console.warn(
260
+ `[sandbox] e2b lifetime extension clamped: requested ${ms}ms exceeds the plan cap ${maxMs}ms — ` +
261
+ `the kill-clock backstop fires at the cap, not the requested horizon. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`,
262
+ );
263
+ }
264
+ await sb.setTimeout(Math.min(ms, maxMs));
249
265
  },
250
266
  // Push a freshly-resolved egress policy onto the live sandbox via E2B's
251
267
  // native `updateNetwork` — the E2B analogue of Vercel's `update({
@@ -317,6 +333,15 @@ export const e2bProviderDef: SandboxProviderDef = {
317
333
  const sandboxOpts = {
318
334
  ...rest,
319
335
  timeoutMs: Math.min(timeoutMs, maxMs),
336
+ // Lifetime-deadline expiry PAUSES instead of killing, and the next
337
+ // incoming connection auto-resumes (recycle forensics 2026-08-23:
338
+ // uncontrolled kill-clock loss was the dominant machine-end cause
339
+ // ~60:1 — under this policy a missed extension becomes a resumable
340
+ // park our redial paths revive). CREATE-time policy, persistent for
341
+ // the sandbox's life — connect() takes no lifecycle. NOTE: this is
342
+ // e2b's `lifecycle` contract, NOT the deprecated `autoPause` flag
343
+ // (which the create body would silently drop).
344
+ lifecycle: { onTimeout: "pause" as const, autoResume: true },
320
345
  ...(networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}),
321
346
  };
322
347
  // Boot template resolution, in priority order:
@@ -339,6 +364,9 @@ export const e2bProviderDef: SandboxProviderDef = {
339
364
  );
340
365
  },
341
366
  reconnect: async (sandboxId) => makeE2bSandboxProvider(
367
+ // The pause-on-timeout policy is create-time and persistent — connect
368
+ // takes no lifecycle options (the old per-connect autoPause flag is
369
+ // deprecated API-side).
342
370
  await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 60 * 60 * 1000 }),
343
371
  ),
344
372
  killAll: async () => {
package/src/sandbox.ts CHANGED
@@ -54,7 +54,7 @@ export type { SandboxCreateOpts, OwnedSandbox } from "./sandbox/provider-def.js"
54
54
  export { parseSseExecStream } from "./sandbox/exec-stream.js";
55
55
  export type { ParseSseExecStreamOptions } from "./sandbox/exec-stream.js";
56
56
 
57
- export { makeSandboxProvider } from "./sandbox/providers/e2b.js";
57
+ export { makeSandboxProvider, e2bMaxSandboxMs } from "./sandbox/providers/e2b.js";
58
58
  export { makeDesktopSandboxProvider } from "./sandbox/providers/desktop.js";
59
59
  export { VERCEL_MAX_TAGS, buildVercelTags } from "./sandbox/providers/vercel.js";
60
60
  export { makeLocalSandboxProvider } from "./sandbox/providers/local.js";
@@ -147,6 +147,19 @@ export interface AgentMessageTaskNotification extends AgentMessageBase {
147
147
  usage?: { tokens?: number; toolUses?: number; durationMs?: number };
148
148
  }
149
149
 
150
+ /** HARNESS-authored notice text — content the CLI composed itself rather
151
+ * than the model speaking: slash-command stdout, model/skills advisories,
152
+ * queued-input notes. claude-code marks these structurally (assistant
153
+ * events whose `message.model` is `"<synthetic>"`), and the normaliser
154
+ * forwards them under this kind so downstream persists them as collapsed
155
+ * system notices instead of agent prose (the 2026-08-22 "(session model)"
156
+ * / skills-reminder leak: harness plumbing rendered as message content).
157
+ * Additive kind: existing producers never emit it. */
158
+ export interface AgentMessageHarnessNotice extends AgentMessageBase {
159
+ type: "harness_notice";
160
+ text: string;
161
+ }
162
+
150
163
  export type AgentMessage =
151
164
  | AgentMessageInit
152
165
  | AgentMessageText
@@ -159,7 +172,8 @@ export type AgentMessage =
159
172
  | AgentMessageUsage
160
173
  | AgentMessageUsageDelta
161
174
  | AgentMessagePlan
162
- | AgentMessageTaskNotification;
175
+ | AgentMessageTaskNotification
176
+ | AgentMessageHarnessNotice;
163
177
 
164
178
  /** Status block the agent emits to signal iteration completion or blockers. */
165
179
  export interface AgentStatus {