@nathapp/nax-agent 0.88.0-canary.1 → 0.88.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 (70) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/coding-tools/coding-tool-support.d.ts +16 -0
  3. package/dist/coding-tools/coding-tool-support.js +12 -0
  4. package/dist/config/schemas-sandbox.d.ts +1 -0
  5. package/dist/config/schemas-sandbox.js +9 -0
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/internal/process-kill.d.ts +6 -2
  9. package/dist/internal/process-kill.js +6 -2
  10. package/dist/internal.d.ts +1 -0
  11. package/dist/internal.js +1 -0
  12. package/dist/mcp/connect.js +27 -1
  13. package/dist/mcp/types.d.ts +8 -0
  14. package/dist/native/auth.d.ts +2 -2
  15. package/dist/native/auth.js +4 -4
  16. package/dist/native/failed-round-trip-spend.d.ts +16 -0
  17. package/dist/native/failed-round-trip-spend.js +33 -0
  18. package/dist/native/session/partial-spend.d.ts +20 -0
  19. package/dist/native/session/partial-spend.js +14 -0
  20. package/dist/native/session/session.js +11 -4
  21. package/dist/native/session/turn-complete-step.js +23 -3
  22. package/dist/native/session/turn-retry.d.ts +3 -3
  23. package/dist/native/session/turn-retry.js +25 -6
  24. package/dist/native/session/turn-types.d.ts +1 -1
  25. package/dist/native/session-adapter.js +3 -2
  26. package/dist/permissions/bash-lex.js +36 -4
  27. package/dist/sandbox/claude-settings.d.ts +46 -0
  28. package/dist/sandbox/claude-settings.js +76 -0
  29. package/dist/sandbox/index.d.ts +2 -1
  30. package/dist/sandbox/index.js +1 -0
  31. package/dist/sandbox/launcher.js +4 -0
  32. package/dist/sandbox/messages.d.ts +1 -1
  33. package/dist/sandbox/messages.js +15 -3
  34. package/dist/sandbox/policy-builder.js +27 -2
  35. package/dist/sandbox/session-sandbox.d.ts +3 -0
  36. package/dist/sandbox/session-sandbox.js +9 -0
  37. package/dist/sandbox/srt-backend.d.ts +10 -1
  38. package/dist/sandbox/srt-backend.js +14 -4
  39. package/dist/sandbox/types.d.ts +23 -0
  40. package/dist/session/agent-session-options.js +1 -1
  41. package/dist/session/agent-session-types.d.ts +1 -1
  42. package/dist/session/native-backend-options.js +5 -3
  43. package/dist/session/session-backend.d.ts +9 -0
  44. package/dist/session/session-tool-support.d.ts +1 -1
  45. package/dist/session/session-tool-support.js +4 -2
  46. package/dist/session/session-types.d.ts +10 -0
  47. package/dist/tools/bash-canonical.d.ts +10 -0
  48. package/dist/tools/bash-canonical.js +207 -0
  49. package/dist/tools/bash.d.ts +7 -1
  50. package/dist/tools/bash.js +18 -3
  51. package/dist/tools/index.d.ts +1 -0
  52. package/dist/tools/index.js +1 -0
  53. package/dist/tools/nax-revert.d.ts +12 -0
  54. package/dist/tools/nax-revert.js +2 -2
  55. package/dist/tools/nax-stash-restore.d.ts +9 -0
  56. package/dist/tools/nax-stash-restore.js +105 -0
  57. package/dist/tools/nax-state-guard.js +16 -30
  58. package/dist/tools/nax-verb-gaps.d.ts +3 -0
  59. package/dist/tools/nax-verb-gaps.js +32 -0
  60. package/dist/tools/policy-bash.js +39 -6
  61. package/dist/tools/policy.js +9 -0
  62. package/dist/tools/provider-advertise.d.ts +13 -0
  63. package/dist/tools/provider-advertise.js +41 -1
  64. package/dist/tools/provider-instructions.d.ts +33 -0
  65. package/dist/tools/provider-instructions.js +63 -0
  66. package/dist/tools/provider-types.d.ts +12 -0
  67. package/dist/tools/run-command.d.ts +15 -0
  68. package/dist/tools/run-command.js +16 -1
  69. package/dist/tools/tool-audit.d.ts +1 -1
  70. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
5
5
  release may change the public API.
6
6
 
7
+ ## [0.88.0] - 2026-10-16
8
+
9
+ - No changes in this package. Released in lockstep with the other nax packages.
10
+
11
+ ## [0.88.0-canary.2] - 2026-10-15
12
+
13
+ - No changes in this package. Released in lockstep with the other nax packages.
14
+
7
15
  ## [0.88.0-canary.1] - 2026-10-13
8
16
 
9
17
  - Bash guard for nax run state (#2490): `git update-index --skip-worktree` / `--assume-unchanged` (and the `--no-` forms) on a `.nax` path, `git stash` / `push` / `pop` / `apply` in a repo that holds a `.nax/` directory (unless the pathspec excludes `.nax`), and `git checkout` / `restore` of a `.nax` path are now refused in every segment of a compound command, in gated and raw mode, also behind `git -C`/`-c`, env assignments, `env`/`command`/`nice`/`timeout`/`xargs` wrappers and `bash -c`/`eval`, plus a coarse text backstop for wrappers it cannot enumerate. `RULE_SET_VERSION` is now 4 (git global options, `-c alias.*` and more wrappers are resolved when locating the subcommand).
@@ -32,6 +32,16 @@ export interface CodingToolSupport {
32
32
  readonly tools: readonly CodingTool[];
33
33
  readonly surface: ToolSurfaceDraft;
34
34
  };
35
+ /** nax#2500: the dispatch launcher's sandbox as data, attached by the nax resolver for ACP opens. */
36
+ readonly acpSandbox?: import("#src/sandbox/types").SessionSandbox;
37
+ /**
38
+ * The rendered `## MCP servers` section (nax#2517), built by the nax resolver
39
+ * with `renderProviderInstructions` from this hop's resolved provider
40
+ * instructions. The session open folds it into the session's `system` field
41
+ * (native) or hands it to the ACP tool host. ABSENT when no attached provider
42
+ * contributed instructions, so "no section" and "an empty one" stay distinct.
43
+ */
44
+ readonly providerInstructions?: string;
35
45
  }
36
46
  export declare function buildCodingToolSupport(args: {
37
47
  root?: string;
@@ -134,6 +144,12 @@ export declare function buildCodingToolSupport(args: {
134
144
  launcher?: CommandLauncher;
135
145
  /** US-001: forwarded into ToolRunContext so Bash/Exec can SIGKILL on cancel. */
136
146
  abortSignal?: AbortSignal;
147
+ /**
148
+ * The rendered `## MCP servers` section for this hop (nax#2517), produced by
149
+ * the resolver from `ResolvedProviderTools.instructions`; returned on the
150
+ * support unchanged.
151
+ */
152
+ providerInstructions?: string;
137
153
  }): CodingToolSupport | undefined;
138
154
  /**
139
155
  * Ledger session name.
@@ -144,8 +144,20 @@ export function buildCodingToolSupport(args) {
144
144
  auditSink: sink,
145
145
  declaredTools: args.declared,
146
146
  ...(args.toolSurface !== undefined ? { toolSurface: args.toolSurface } : {}),
147
+ ...providerInstructionsField(args),
147
148
  };
148
149
  }
150
+ /**
151
+ * The support's optional `providerInstructions` (#2517) as a spreadable field —
152
+ * ABSENT when unset, so `"providerInstructions" in support` answers "is there
153
+ * an MCP section?". A helper rather than an inline conditional because
154
+ * `buildCodingToolSupport` sits at its complexity baseline.
155
+ */
156
+ function providerInstructionsField(args) {
157
+ if (args.providerInstructions === undefined)
158
+ return {};
159
+ return { providerInstructions: args.providerInstructions };
160
+ }
149
161
  /** The audit sink a support resolution records through; no dir ⇒ a non-recording sink. */
150
162
  function supportSink(args) {
151
163
  if (args.auditDir === undefined)
@@ -24,6 +24,7 @@ export declare const SandboxConfigSchema: z.ZodObject<{
24
24
  }, z.core.$strip>>;
25
25
  network: z.ZodPrefault<z.ZodObject<{
26
26
  allowedDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ allowLocalBinding: z.ZodOptional<z.ZodBoolean>;
27
28
  }, z.core.$strip>>;
28
29
  }, z.core.$strip>;
29
30
  export type SandboxConfig = z.infer<typeof SandboxConfigSchema>;
@@ -38,6 +38,15 @@ export const SandboxConfigSchema = z.object({
38
38
  .object({
39
39
  /** absent = unrestricted (spec S2); [] = no network; a list = allow-list. */
40
40
  allowedDomains: z.array(z.string()).optional(),
41
+ /**
42
+ * US-002 (#2538): let a sandboxed command reach loopback ports.
43
+ *
44
+ * Off when absent, and it only changes anything alongside an
45
+ * `allowedDomains` list: with an open network srt's macOS profile already
46
+ * allows all traffic. srt reads it once per process, from the config given
47
+ * to `SandboxManager.initialize`.
48
+ */
49
+ allowLocalBinding: z.boolean().optional(),
41
50
  })
42
51
  .prefault({}),
43
52
  });
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export type { AfterResponsePatch, AfterResponsePayload, AfterToolPatch, AfterToo
26
26
  export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
27
27
  export { type ApprovalAuditRow, type ApprovalEntry, type ApprovalsFile, type ApprovalsFileRead, type ApprovalsFileState, type ApprovalsTaint, ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, type AskControl, type AskDecidedBy, type AskDecision, type AskLink, type AskLinkOutcome, type AskRequest, type AskResolver, type AskVerdict, appendApproval, appendApprovalAudit, approvalId, approvalsPath, type BashLexResult, type BashRedirect, type BashSegment, type BashSegmentSeparator, type BashToken, type ClearTaintOutcome, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, type MaskResult, maskForPrompt, type PrepareApprovalsStoreOptions, parseRuleList, parseToolExpression, prepareApprovalsStore, type RemovalDecision, type RemovalResult, type ResolvedPermissions, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, type SecretSpan, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
28
28
  export { type AgentGlobOptions, type AgentRuntime, type AgentSpawnOptions, type AgentSpawnResult, type AgentSpawnStdin, getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
29
- export { buildSandboxPolicy, type CommandLauncher, type CommandLauncherOptions, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, type GitLayout, type LaunchRequest, type LaunchResult, type LaunchSpec, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, type ProbeResult, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, type SandboxBackend, type SandboxBackendName, type SandboxNetworkPolicy, type SandboxPolicy, type SandboxPolicyInput, type SandboxRecord, type SandboxState, type SandboxWrapRequest, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
29
+ export { buildSandboxPolicy, type CommandLauncher, type CommandLauncherOptions, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, type GitLayout, type LaunchRequest, type LaunchResult, type LaunchSpec, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, type ProbeResult, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, type SandboxBackend, type SandboxBackendName, type SandboxNetworkPolicy, type SandboxPolicy, type SandboxPolicyInput, type SandboxRecord, type SandboxState, type SandboxWrapRequest, type SessionSandbox, sessionSandboxFrom, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
30
30
  export type { AdapterFailure } from "#src/session/adapter-failure";
31
31
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
32
32
  export { AgentSessionError, type AgentSessionErrorCode } from "#src/session/agent-session-errors";
@@ -42,5 +42,5 @@ export { attachTurnSpend, type FailedTurnSpend, readTurnSpend } from "#src/sessi
42
42
  export type { JSONSchema, ToolDescriptor, } from "#src/session/tool-descriptor";
43
43
  export { createTurnDeadline, type TurnDeadline, } from "#src/session/turn-deadline";
44
44
  export type { TurnEvent, TurnEventSink } from "#src/session/turn-event";
45
- export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
45
+ export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_MAX_CHARS_TOTAL, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, renderProviderInstructions, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
46
46
  export { EMPTY_OWNED_PATHS_POLICY, type OwnedBashCandidate, type OwnedPathsPolicy, } from "#src/tools/owned-paths";
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOut
21
21
  export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
22
22
  export { ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, appendApproval, appendApprovalAudit, approvalId, approvalsPath, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, maskForPrompt, parseRuleList, parseToolExpression, prepareApprovalsStore, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
23
23
  export { getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
24
- export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
24
+ export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, sessionSandboxFrom, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
25
25
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
26
26
  export { AgentSessionError } from "#src/session/agent-session-errors";
27
27
  export { NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
@@ -29,5 +29,5 @@ export { nativeBackend } from "#src/session/native-backend";
29
29
  export { SessionTurnError, } from "#src/session/session-types";
30
30
  export { attachTurnSpend, readTurnSpend } from "#src/session/turn-spend";
31
31
  export { createTurnDeadline, } from "#src/session/turn-deadline";
32
- export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, deleteTool, EXEC_TOOL_NAME, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, mcpRuleAdmits, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, PROVIDER_ID_RE, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
32
+ export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, deleteTool, EXEC_TOOL_NAME, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_MAX_CHARS_TOTAL, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, mcpRuleAdmits, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, PROVIDER_ID_RE, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, renderProviderInstructions, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
33
33
  export { EMPTY_OWNED_PATHS_POLICY, } from "#src/tools/owned-paths";
@@ -12,7 +12,9 @@
12
12
  * 2. Falls back to killing the single process if group kill fails
13
13
  *
14
14
  * @param pid - Process ID (positive, not negative)
15
- * @param signal - Signal to send (SIGTERM, SIGKILL, etc.)
15
+ * @param signal - Signal to send (SIGTERM, SIGKILL, etc.); `0` probes whether the group still has a member
16
+ * @param opts - `singleProcessFallback: false` skips step 2. Pass it once the group leader is known to have
17
+ * exited: the bare pid may have been reused by an unrelated process by then.
16
18
  * @returns true if kill was sent successfully, false if process already exited
17
19
  *
18
20
  * @example
@@ -24,4 +26,6 @@
24
26
  * }
25
27
  * ```
26
28
  */
27
- export declare function killProcessGroup(pid: number, signal: NodeJS.Signals | number): boolean;
29
+ export declare function killProcessGroup(pid: number, signal: NodeJS.Signals | number, opts?: {
30
+ readonly singleProcessFallback?: boolean;
31
+ }): boolean;
@@ -12,7 +12,9 @@
12
12
  * 2. Falls back to killing the single process if group kill fails
13
13
  *
14
14
  * @param pid - Process ID (positive, not negative)
15
- * @param signal - Signal to send (SIGTERM, SIGKILL, etc.)
15
+ * @param signal - Signal to send (SIGTERM, SIGKILL, etc.); `0` probes whether the group still has a member
16
+ * @param opts - `singleProcessFallback: false` skips step 2. Pass it once the group leader is known to have
17
+ * exited: the bare pid may have been reused by an unrelated process by then.
16
18
  * @returns true if kill was sent successfully, false if process already exited
17
19
  *
18
20
  * @example
@@ -24,7 +26,7 @@
24
26
  * }
25
27
  * ```
26
28
  */
27
- export function killProcessGroup(pid, signal) {
29
+ export function killProcessGroup(pid, signal, opts = {}) {
28
30
  // A pid that is not a real child reaches other processes: 0 is our own group,
29
31
  // 1 (as -1) is every process we may signal, and a negative pid is init or a
30
32
  // foreign group. A spawn that failed has no pid, so refuse rather than signal.
@@ -39,6 +41,8 @@ export function killProcessGroup(pid, signal) {
39
41
  const err = error;
40
42
  // ESRCH = no such process — process group doesn't exist, try single process
41
43
  if (err.code === "ESRCH") {
44
+ if (opts.singleProcessFallback === false)
45
+ return false;
42
46
  try {
43
47
  process.kill(pid, signal);
44
48
  return true;
@@ -93,6 +93,7 @@ export * from "#src/tools/git-commit";
93
93
  export * from "#src/tools/grep";
94
94
  export * from "#src/tools/index";
95
95
  export { _instructionAccessDeps } from "#src/tools/instruction-access";
96
+ export { _naxStateGuardDeps } from "#src/tools/nax-stash-restore";
96
97
  export * from "#src/tools/owned-paths";
97
98
  export * from "#src/tools/package-managers";
98
99
  export * from "#src/tools/policy";
package/dist/internal.js CHANGED
@@ -93,6 +93,7 @@ export * from "#src/tools/git-commit";
93
93
  export * from "#src/tools/grep";
94
94
  export * from "#src/tools/index";
95
95
  export { _instructionAccessDeps } from "#src/tools/instruction-access";
96
+ export { _naxStateGuardDeps } from "#src/tools/nax-stash-restore";
96
97
  // S3-2: the port (nax supplies the policy behind it from its own module).
97
98
  export * from "#src/tools/owned-paths";
98
99
  export * from "#src/tools/package-managers";
@@ -83,6 +83,15 @@ function createHandle(config) {
83
83
  throw new McpConnectError(`MCP connect failed: ${messageOf(error)}`);
84
84
  }
85
85
  }
86
+ /**
87
+ * The server's `initialize` instructions (#2517), or `undefined` for anything
88
+ * that is not a non-empty string: an empty one would render an empty prompt
89
+ * section, and a `getInstructions()` that returns nothing is the same silence.
90
+ */
91
+ function serverInstructions(client) {
92
+ const text = client.getInstructions();
93
+ return typeof text === "string" && text !== "" ? text : undefined;
94
+ }
86
95
  export async function connectMcp(config, opts) {
87
96
  const handle = createHandle(config);
88
97
  const client = new Client({ name: opts.clientInfo.name, version: opts.clientInfo.version });
@@ -93,9 +102,25 @@ export async function connectMcp(config, opts) {
93
102
  try {
94
103
  if (signal.aborted)
95
104
  throw new McpConnectError("MCP connect failed: aborted");
105
+ if (config.kind === "stdio" && opts.onSpawn !== undefined) {
106
+ const start = handle.transport.start;
107
+ let notified = false;
108
+ handle.transport.start = async function () {
109
+ await start.call(this);
110
+ const pid = handle.pid();
111
+ if (pid !== null && !notified) {
112
+ notified = true;
113
+ opts.onSpawn?.(pid);
114
+ }
115
+ };
116
+ }
96
117
  await client.connect(handle.transport, { signal, timeout: opts.timeoutMs });
97
118
  const tools = await listAll(client, signal, opts.timeoutMs);
98
- return liveConnection(config, client, handle, tools, { graceMs, life });
119
+ return liveConnection(config, client, handle, tools, {
120
+ graceMs,
121
+ life,
122
+ instructions: serverInstructions(client),
123
+ });
99
124
  }
100
125
  catch (error) {
101
126
  await closeTransport(client, handle, graceMs, life);
@@ -127,6 +152,7 @@ function liveConnection(config, client, handle, tools, state) {
127
152
  return handle.pid();
128
153
  },
129
154
  tools,
155
+ ...(state.instructions !== undefined ? { instructions: state.instructions } : {}),
130
156
  call(name, input, opts) {
131
157
  if (closing !== undefined || life.exited)
132
158
  return Promise.reject(new McpCallError(`MCP call ${name} failed: the connection is closed`));
@@ -31,6 +31,12 @@ export interface McpConnection {
31
31
  /** The stdio child process pid; null for HTTP. Read before close() — the SDK may clear it after. */
32
32
  readonly pid: number | null;
33
33
  readonly tools: readonly McpToolInfo[];
34
+ /**
35
+ * The server's own `initialize` instructions — how to use it, in its words
36
+ * (#2517). Absent when it sent none (or an empty string): an MCP server that
37
+ * says nothing must not produce an empty prompt section.
38
+ */
39
+ readonly instructions?: string;
34
40
  /** Server-side tool errors are `isError` results; transport failures throw McpCallError. */
35
41
  call(name: string, input: unknown, opts: McpCallOptions): Promise<McpCallResult>;
36
42
  /** stdio only: called once when the process exits without close() having been called. */
@@ -45,6 +51,8 @@ export interface ConnectMcpOptions {
45
51
  readonly name: string;
46
52
  readonly version: string;
47
53
  };
54
+ /** stdio only: called once after spawn, before the initialize request is sent. */
55
+ readonly onSpawn?: (pid: number) => void;
48
56
  /** stdio: how long close() waits for the process before SIGKILL. Default 3000. */
49
57
  readonly closeGraceMs?: number;
50
58
  }
@@ -29,8 +29,8 @@ export declare const _authDeps: {
29
29
  ambientAuthAvailable: typeof ambientAuthAvailable;
30
30
  providerIds: () => Promise<string[]>;
31
31
  /**
32
- * Injectable timer pair — lets tests drive `anyAmbientCredential`'s
33
- * expiry branch off a virtual clock instead of waiting the full 2s
32
+ * Injectable timer pair — lets tests drive ambient credential probes'
33
+ * expiry branches off a virtual clock instead of waiting the full 2s
34
34
  * production timeout. Mirrors `_heartbeatDeps` / `_idleWatchdogDeps`.
35
35
  *
36
36
  * @internal
@@ -41,8 +41,8 @@ export const _authDeps = {
41
41
  ambientAuthAvailable,
42
42
  providerIds: async () => (await defaultProviders()).map((provider) => provider.id),
43
43
  /**
44
- * Injectable timer pair — lets tests drive `anyAmbientCredential`'s
45
- * expiry branch off a virtual clock instead of waiting the full 2s
44
+ * Injectable timer pair — lets tests drive ambient credential probes'
45
+ * expiry branches off a virtual clock instead of waiting the full 2s
46
46
  * production timeout. Mirrors `_heartbeatDeps` / `_idleWatchdogDeps`.
47
47
  *
48
48
  * @internal
@@ -301,14 +301,14 @@ export async function providersWithoutCredentials(providerIds) {
301
301
  })).then((missing) => missing.filter((id) => id !== undefined));
302
302
  let timer;
303
303
  const expiry = new Promise((resolve) => {
304
- timer = setTimeout(() => resolve([]), AMBIENT_PROBE_TIMEOUT_MS);
304
+ timer = _authDeps.setTimeout(() => resolve([]), AMBIENT_PROBE_TIMEOUT_MS);
305
305
  });
306
306
  try {
307
307
  return await Promise.race([sweep, expiry]);
308
308
  }
309
309
  finally {
310
310
  if (timer !== undefined)
311
- clearTimeout(timer);
311
+ _authDeps.clearTimeout(timer);
312
312
  }
313
313
  }
314
314
  /**
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Prices the tokens a failed or aborted round trip was billed for and attaches
3
+ * them to the thrown error (`session/partial-spend.ts`), where the turn loop's
4
+ * `completeWithRecovery` folds them into the turn's accumulated usage.
5
+ *
6
+ * nax-ai's `collectStream` hands the partial usage over on the error
7
+ * (`partialUsageOf`); only the adapter holds the rate card, so pricing happens
8
+ * here, once, with the same `priceCall` the success path uses.
9
+ */
10
+ import type { Pricing } from "#src/cost/standard-types";
11
+ /**
12
+ * Runs one round trip; if it throws, prices whatever it was billed first and
13
+ * attaches that to the error, which is rethrown untouched. A wrapper (rather
14
+ * than a catch at the call site) keeps `sendTurn` under the complexity ratchet.
15
+ */
16
+ export declare function billedOnFailure<T>(rates: Pricing, roundTrip: () => Promise<T>): Promise<T>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Prices the tokens a failed or aborted round trip was billed for and attaches
3
+ * them to the thrown error (`session/partial-spend.ts`), where the turn loop's
4
+ * `completeWithRecovery` folds them into the turn's accumulated usage.
5
+ *
6
+ * nax-ai's `collectStream` hands the partial usage over on the error
7
+ * (`partialUsageOf`); only the adapter holds the rate card, so pricing happens
8
+ * here, once, with the same `priceCall` the success path uses.
9
+ */
10
+ import { partialUsageOf } from "@nathapp/nax-ai";
11
+ import { priceCall } from "#src/cost/core/index";
12
+ import { attachPartialSpend } from "./session/partial-spend.js";
13
+ function recordFailedRoundTripSpend(err, rates) {
14
+ const usage = partialUsageOf(err);
15
+ if (usage === undefined)
16
+ return;
17
+ const { costUsd, resolvedRates } = priceCall(usage, rates);
18
+ attachPartialSpend(err, { usage, costUsd, rates: resolvedRates });
19
+ }
20
+ /**
21
+ * Runs one round trip; if it throws, prices whatever it was billed first and
22
+ * attaches that to the error, which is rethrown untouched. A wrapper (rather
23
+ * than a catch at the call site) keeps `sendTurn` under the complexity ratchet.
24
+ */
25
+ export async function billedOnFailure(rates, roundTrip) {
26
+ try {
27
+ return await roundTrip();
28
+ }
29
+ catch (err) {
30
+ recordFailedRoundTripSpend(err, rates);
31
+ throw err;
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Spend billed by a round trip that then failed.
3
+ *
4
+ * A failed or aborted request still bills the tokens it streamed (nax-ai's
5
+ * `partialUsageOf`). The adapter's `complete` closure is the only place that
6
+ * can price them, but it can only throw, so the priced spend rides on the
7
+ * thrown error's identity (a WeakMap, like `turn-spend.ts`) and the turn loop's
8
+ * `completeWithRecovery` folds it into the turn accumulator. `take` removes the
9
+ * entry, so an error reaching the fold twice cannot count twice.
10
+ */
11
+ import type { PricingRates, TokenUsage } from "#src/cost/standard-types";
12
+ export interface PartialSpend {
13
+ readonly usage: TokenUsage;
14
+ readonly costUsd: number;
15
+ readonly rates?: PricingRates;
16
+ }
17
+ /** Attaches the priced partial spend of a failed round trip to the error it threw. */
18
+ export declare function attachPartialSpend(err: unknown, spend: PartialSpend): void;
19
+ /** Removes and returns the partial spend attached to `err`, if any. */
20
+ export declare function takePartialSpend(err: unknown): PartialSpend | undefined;
@@ -0,0 +1,14 @@
1
+ const partialByError = new WeakMap();
2
+ /** Attaches the priced partial spend of a failed round trip to the error it threw. */
3
+ export function attachPartialSpend(err, spend) {
4
+ if (typeof err === "object" && err !== null)
5
+ partialByError.set(err, spend);
6
+ }
7
+ /** Removes and returns the partial spend attached to `err`, if any. */
8
+ export function takePartialSpend(err) {
9
+ if (typeof err !== "object" || err === null)
10
+ return undefined;
11
+ const spend = partialByError.get(err);
12
+ partialByError.delete(err);
13
+ return spend;
14
+ }
@@ -83,12 +83,19 @@ function openTranscriptStore(name, opts) {
83
83
  /**
84
84
  * Records, or clears, the session's system prompt at open. A helper so that
85
85
  * openNativeSession gains no branch.
86
+ *
87
+ * `providerInstructions` (the rendered `## MCP servers` section, #2517) is
88
+ * folded in HERE rather than kept in a map of its own: the model sees one
89
+ * `system` string, and every consumer of `state.systemPrompts`
90
+ * (`systemFieldFor`, the adapter's context-budget setup) keeps working
91
+ * unchanged.
86
92
  */
87
- function recordSystemPrompt(state, name, systemPrompt) {
88
- if (systemPrompt === undefined)
93
+ function recordSystemPrompt(state, name, prompts) {
94
+ const parts = [prompts.systemPrompt, prompts.providerInstructions].filter((part) => part !== undefined && part !== "");
95
+ if (parts.length === 0)
89
96
  state.systemPrompts.delete(name);
90
97
  else
91
- state.systemPrompts.set(name, systemPrompt);
98
+ state.systemPrompts.set(name, parts.join("\n\n"));
92
99
  }
93
100
  /** The request field for the session's system prompt: `{ system }`, or `{}` when it has none. */
94
101
  export function systemFieldFor(state, sessionName) {
@@ -119,7 +126,7 @@ export async function openNativeSession(state, name, opts) {
119
126
  state.carryHistoryAcrossModels.add(name);
120
127
  else
121
128
  state.carryHistoryAcrossModels.delete(name);
122
- recordSystemPrompt(state, name, opts.systemPrompt);
129
+ recordSystemPrompt(state, name, opts);
123
130
  const instructions = await openRepositoryInstructions(name, opts, store);
124
131
  state.repositoryInstructions.set(name, instructions);
125
132
  state.transcripts.set(name, {
@@ -20,6 +20,8 @@
20
20
  */
21
21
  import { getSafeLogger } from "#src/infra/index";
22
22
  import { applyHistoryPatch } from "./loop-events/cache-boundary.js";
23
+ import { takePartialSpend } from "./partial-spend.js";
24
+ import { usageBeat } from "./turn-accumulator.js";
23
25
  import { runOverflowCompaction } from "./turn-compaction-step.js";
24
26
  import { realSleep, retryTransportFault } from "./turn-retry.js";
25
27
  /**
@@ -89,8 +91,26 @@ export async function completeWithRecovery(args) {
89
91
  honoured = wire.honoured;
90
92
  return deps.complete(wire.messages, tools, options, turnEvents.deltaSink(roundTrip));
91
93
  };
94
+ // A failed round trip was still billed for what it streamed. Folding it in
95
+ // here, per attempt, is what makes a retry count both attempts and an
96
+ // aborted or timed-out one count what it cost; `takePartialSpend` consumes
97
+ // the entry, and the retry loop rethrows the error it was given without
98
+ // re-entering this wrapper, so nothing counts twice.
99
+ const countedRequest = async (msgs) => {
100
+ try {
101
+ return await request(msgs);
102
+ }
103
+ catch (err) {
104
+ const spend = takePartialSpend(err);
105
+ if (spend !== undefined) {
106
+ usage.add(spend.usage, spend.costUsd, spend.rates);
107
+ deps.onActivity?.(usageBeat(spend.usage, spend.costUsd));
108
+ }
109
+ throw err;
110
+ }
111
+ };
92
112
  try {
93
- res = await request(messages);
113
+ res = await countedRequest(messages);
94
114
  }
95
115
  catch (err) {
96
116
  // Written as one guarded `if` (not a separate `canRetry` boolean) so
@@ -109,7 +129,7 @@ export async function completeWithRecovery(args) {
109
129
  if (deps.transportRetry === undefined)
110
130
  throw err;
111
131
  res = await retryTransportFault(err, {
112
- attempt: () => request(messages),
132
+ attempt: () => countedRequest(messages),
113
133
  config: deps.transportRetry,
114
134
  deadline: deps.deadline,
115
135
  signal,
@@ -161,7 +181,7 @@ export async function completeWithRecovery(args) {
161
181
  turnEvents.emit({ type: "compaction", reason: "overflow" });
162
182
  // Retried once. A second overflow propagates: compacting further would be
163
183
  // guessing, and the failure now carries a correct diagnosis.
164
- res = await request(messages);
184
+ res = await countedRequest(messages);
165
185
  }
166
186
  }
167
187
  return { res, messages, compacted, honoured, boundary: compacted };
@@ -80,9 +80,9 @@ export declare function turnRetryFitsBudget(delayMs: number, remainingMs: number
80
80
  * nax-ai's retry.ts `abortableSleep` so a pending backoff cannot outlive the
81
81
  * caller's own cancellation and go on to start a request nobody wants.
82
82
  */
83
- export declare function abortableSleep(ms: number, sleep: (ms: number) => Promise<void>, signal?: AbortSignal): Promise<void>;
83
+ export declare function abortableSleep(ms: number, sleep: (ms: number, signal?: AbortSignal) => Promise<void>, signal?: AbortSignal): Promise<void>;
84
84
  /** The real timer. Named and exported so the only production wiring of it is one reference. */
85
- export declare function realSleep(ms: number): Promise<void>;
85
+ export declare function realSleep(ms: number, signal?: AbortSignal): Promise<void>;
86
86
  export interface TurnRetryDeps<T> {
87
87
  /** Re-issues the round trip. Called once per retry, never for the triggering attempt. */
88
88
  readonly attempt: () => Promise<T>;
@@ -99,7 +99,7 @@ export interface TurnRetryDeps<T> {
99
99
  * and leaving it optional let a test omit it and silently wait out a real
100
100
  * multi-second backoff. Required makes that a compile error instead.
101
101
  */
102
- readonly sleep: (ms: number) => Promise<void>;
102
+ readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
103
103
  /** Injectable jitter source, for deterministic tests. Defaults to Math.random. */
104
104
  readonly random?: () => number;
105
105
  /** Fired once per retry, before the backoff delay, with a 1-based retry number. */
@@ -104,7 +104,7 @@ export function turnRetryFitsBudget(delayMs, remainingMs) {
104
104
  return remainingMs === undefined || delayMs <= remainingMs;
105
105
  }
106
106
  function abortError(signal) {
107
- return signal.reason ?? new DOMException("Aborted", "AbortError");
107
+ return signal?.reason ?? new DOMException("Aborted", "AbortError");
108
108
  }
109
109
  /**
110
110
  * A sleep that resolves early, and rejects, when `signal` fires — mirrors
@@ -113,21 +113,40 @@ function abortError(signal) {
113
113
  */
114
114
  export function abortableSleep(ms, sleep, signal) {
115
115
  if (signal === undefined)
116
- return sleep(ms);
116
+ return sleep(ms, signal);
117
117
  if (signal.aborted)
118
118
  return Promise.reject(abortError(signal));
119
119
  return new Promise((resolve, reject) => {
120
120
  const onAbort = () => reject(abortError(signal));
121
121
  signal.addEventListener("abort", onAbort, { once: true });
122
- sleep(ms).then(() => {
122
+ // Keep the race for injected sleeps that do not honour cancellation.
123
+ Promise.resolve()
124
+ .then(() => sleep(ms, signal))
125
+ .then(() => {
123
126
  signal.removeEventListener("abort", onAbort);
124
127
  resolve();
125
- }, reject);
128
+ }, (error) => {
129
+ signal.removeEventListener("abort", onAbort);
130
+ reject(error);
131
+ });
126
132
  });
127
133
  }
128
134
  /** The real timer. Named and exported so the only production wiring of it is one reference. */
129
- export function realSleep(ms) {
130
- return new Promise((resolve) => setTimeout(resolve, ms));
135
+ export function realSleep(ms, signal) {
136
+ if (signal?.aborted)
137
+ return Promise.reject(abortError(signal));
138
+ return new Promise((resolve, reject) => {
139
+ const onAbort = () => {
140
+ clearTimeout(timer);
141
+ reject(abortError(signal));
142
+ };
143
+ // Cancellable timer: abort must release the pending backoff immediately.
144
+ const timer = setTimeout(() => {
145
+ signal?.removeEventListener("abort", onAbort);
146
+ resolve();
147
+ }, ms);
148
+ signal?.addEventListener("abort", onAbort, { once: true });
149
+ });
131
150
  }
132
151
  /**
133
152
  * Retries `attempt` while the thrown error is a transport/overloaded fault,
@@ -97,7 +97,7 @@ export interface TurnDeps {
97
97
  * actually wait (forbidden-patterns-tests.md). Absent uses a real timer —
98
98
  * a real session always wants to actually wait.
99
99
  */
100
- sleep?: (ms: number) => Promise<void>;
100
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
101
101
  /**
102
102
  * Live repetition-breaker instance (nax#2013, session-lifetime since #2047). Absent disables the breaker.
103
103
  */
@@ -15,6 +15,7 @@ import { _adapterDeps, authFields, isProtocolStreamError } from "./adapter-deps.
15
15
  import { _clientDeps, getNativeClient } from "./client.js";
16
16
  import { createSessionCredentialStore } from "./credentials/session-source.js";
17
17
  import { toAdapterFailure } from "./errors.js";
18
+ import { billedOnFailure } from "./failed-round-trip-spend.js";
18
19
  import { buildRateCard, NATIVE_AGENT, parseNativeModel, resolveContextWindow, toThinkingLevel } from "./models.js";
19
20
  import { createInstructionInteraction } from "./session/instruction-interaction.js";
20
21
  import { closeNativeSession, createNativeSessionState, markNativeTurnOutcome, openNativeSession, systemFieldFor, } from "./session/session.js";
@@ -305,7 +306,7 @@ export class NativeSessionAdapter {
305
306
  const requestThinking = requestOptions?.thinking === false ? undefined : thinking;
306
307
  try {
307
308
  instructionInteraction.acknowledge();
308
- const res = await streamComplete(client, resolved, {
309
+ const res = await billedOnFailure(rates, () => streamComplete(client, resolved, {
309
310
  messages,
310
311
  ...(tools.length > 0 ? { tools } : {}),
311
312
  sessionId,
@@ -324,7 +325,7 @@ export class NativeSessionAdapter {
324
325
  // to reuse the entry, so a cache write there costs more than it
325
326
  // saves.
326
327
  cacheRetention: "short",
327
- }, onDelta);
328
+ }, onDelta));
328
329
  const usage = res.usage;
329
330
  // Single `priceCall` invocation: `costUsd` and `resolvedRates`
330
331
  // come from the same call so they cannot diverge — the