@nathapp/nax-agent 0.1.0 → 0.3.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 (129) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -2
  3. package/dist/coding-tools/coding-tool-sandbox.d.ts +3 -0
  4. package/dist/coding-tools/coding-tool-sandbox.js +14 -3
  5. package/dist/coding-tools/coding-tool-support.d.ts +3 -0
  6. package/dist/coding-tools/coding-tool-support.js +2 -0
  7. package/dist/index.d.ts +21 -3
  8. package/dist/index.js +13 -3
  9. package/dist/infra/stderr-tail.d.ts +8 -0
  10. package/dist/infra/stderr-tail.js +48 -0
  11. package/dist/internal/redact.d.ts +23 -0
  12. package/dist/internal/redact.js +60 -0
  13. package/dist/internal.d.ts +7 -0
  14. package/dist/internal.js +8 -0
  15. package/dist/native/adapter-deps.d.ts +3 -3
  16. package/dist/native/adapter-deps.js +3 -3
  17. package/dist/native/client.d.ts +7 -1
  18. package/dist/native/client.js +1 -1
  19. package/dist/native/credentials/change-guard.d.ts +8 -1
  20. package/dist/native/credentials/change-guard.js +1 -1
  21. package/dist/native/credentials/fingerprint.d.ts +4 -1
  22. package/dist/native/credentials/fingerprint.js +6 -3
  23. package/dist/native/credentials/session-source.d.ts +12 -0
  24. package/dist/native/credentials/session-source.js +33 -0
  25. package/dist/native/index.d.ts +3 -1
  26. package/dist/native/index.js +2 -1
  27. package/dist/native/session/loop-events/types.d.ts +1 -1
  28. package/dist/native/session/loop-handlers.d.ts +3 -0
  29. package/dist/native/session/loop-handlers.js +1 -1
  30. package/dist/native/session/memory-transcript-store.d.ts +12 -0
  31. package/dist/native/session/memory-transcript-store.js +36 -0
  32. package/dist/native/session/session.d.ts +138 -96
  33. package/dist/native/session/session.js +107 -127
  34. package/dist/native/session/transcript-identity.d.ts +39 -0
  35. package/dist/native/session/transcript-identity.js +81 -0
  36. package/dist/native/session/transcript-store.d.ts +22 -21
  37. package/dist/native/session/transcript-store.js +59 -68
  38. package/dist/native/session/transcript-types.d.ts +54 -0
  39. package/dist/native/session/transcript-types.js +7 -0
  40. package/dist/native/session/truncation-handler.d.ts +4 -3
  41. package/dist/native/session/truncation-handler.js +7 -10
  42. package/dist/native/session/turn-complete-step.d.ts +4 -1
  43. package/dist/native/session/turn-complete-step.js +11 -4
  44. package/dist/native/session/turn-event-emitter.d.ts +48 -0
  45. package/dist/native/session/turn-event-emitter.js +134 -0
  46. package/dist/native/session/turn-loop-round-trip.d.ts +5 -2
  47. package/dist/native/session/turn-loop-round-trip.js +8 -4
  48. package/dist/native/session/turn-loop.js +27 -13
  49. package/dist/native/session/turn-result.d.ts +1 -1
  50. package/dist/native/session/turn-result.js +1 -1
  51. package/dist/native/session/turn-tool-batch.d.ts +3 -0
  52. package/dist/native/session/turn-tool-batch.js +14 -6
  53. package/dist/native/session/turn-types.d.ts +17 -1
  54. package/dist/native/session-adapter.d.ts +22 -4
  55. package/dist/native/session-adapter.js +79 -35
  56. package/dist/native/stream-complete.d.ts +17 -0
  57. package/dist/native/stream-complete.js +40 -0
  58. package/dist/permissions/approvals-link.js +5 -4
  59. package/dist/permissions/ask-chain.d.ts +1 -1
  60. package/dist/permissions/ask.d.ts +6 -0
  61. package/dist/permissions/ask.js +6 -0
  62. package/dist/permissions/index.d.ts +1 -1
  63. package/dist/permissions/index.js +1 -1
  64. package/dist/sandbox/policy-builder.d.ts +6 -1
  65. package/dist/sandbox/policy-builder.js +18 -14
  66. package/dist/session/agent-session-deps.d.ts +12 -0
  67. package/dist/session/agent-session-deps.js +28 -0
  68. package/dist/session/agent-session-errors.d.ts +11 -0
  69. package/dist/session/agent-session-errors.js +12 -0
  70. package/dist/session/agent-session-options.d.ts +12 -0
  71. package/dist/session/agent-session-options.js +96 -0
  72. package/dist/session/agent-session-resume.d.ts +14 -0
  73. package/dist/session/agent-session-resume.js +49 -0
  74. package/dist/session/agent-session-turn.d.ts +41 -0
  75. package/dist/session/agent-session-turn.js +177 -0
  76. package/dist/session/agent-session-types.d.ts +182 -0
  77. package/dist/session/agent-session-types.js +1 -0
  78. package/dist/session/agent-session.d.ts +8 -0
  79. package/dist/session/agent-session.js +184 -0
  80. package/dist/session/native-backend-options.d.ts +24 -0
  81. package/dist/session/native-backend-options.js +81 -0
  82. package/dist/session/native-backend.d.ts +4 -0
  83. package/dist/session/native-backend.js +106 -0
  84. package/dist/session/pending-asks.d.ts +24 -0
  85. package/dist/session/pending-asks.js +77 -0
  86. package/dist/session/session-ask-link.d.ts +33 -0
  87. package/dist/session/session-ask-link.js +53 -0
  88. package/dist/session/session-ask-port.d.ts +12 -0
  89. package/dist/session/session-ask-port.js +64 -0
  90. package/dist/session/session-backend.d.ts +79 -0
  91. package/dist/session/session-backend.js +1 -0
  92. package/dist/session/session-event-channel.d.ts +17 -0
  93. package/dist/session/session-event-channel.js +132 -0
  94. package/dist/session/session-interaction.d.ts +20 -0
  95. package/dist/session/session-interaction.js +161 -0
  96. package/dist/session/session-tool-support.d.ts +48 -0
  97. package/dist/session/session-tool-support.js +104 -0
  98. package/dist/session/session-types.d.ts +33 -2
  99. package/dist/session/turn-event.d.ts +66 -0
  100. package/dist/session/turn-event.js +1 -0
  101. package/dist/tools/ask-request.js +3 -1
  102. package/dist/tools/bash-cwd.d.ts +3 -3
  103. package/dist/tools/credential-read-deny.d.ts +22 -0
  104. package/dist/tools/credential-read-deny.js +56 -0
  105. package/dist/tools/glob.js +9 -2
  106. package/dist/tools/grep.js +55 -26
  107. package/dist/tools/owned-paths.d.ts +43 -0
  108. package/dist/tools/owned-paths.js +17 -0
  109. package/dist/tools/package-managers.d.ts +3 -0
  110. package/dist/tools/package-managers.js +7 -3
  111. package/dist/tools/policy-bash-raw.d.ts +3 -0
  112. package/dist/tools/policy-bash-raw.js +49 -62
  113. package/dist/tools/policy-command-branch.d.ts +3 -0
  114. package/dist/tools/policy-command-branch.js +2 -1
  115. package/dist/tools/policy-paths-branch.d.ts +14 -8
  116. package/dist/tools/policy-paths-branch.js +20 -19
  117. package/dist/tools/policy.d.ts +10 -7
  118. package/dist/tools/policy.js +9 -7
  119. package/dist/tools/protected-paths.d.ts +6 -6
  120. package/dist/tools/read.js +38 -20
  121. package/dist/tools/registry.d.ts +3 -0
  122. package/dist/tools/run-command-exec.js +1 -0
  123. package/dist/tools/runtime.d.ts +3 -0
  124. package/dist/tools/runtime.js +1 -0
  125. package/dist/tools/scratchpad.js +2 -1
  126. package/dist/tools/types.d.ts +2 -2
  127. package/package.json +2 -2
  128. package/dist/tools/nax-owned-writes.d.ts +0 -135
  129. package/dist/tools/nax-owned-writes.js +0 -259
@@ -16,14 +16,21 @@ import type { AuthStamp } from "#src/session/session-types";
16
16
  /**
17
17
  * Where a credential came from, as reported by `describe`. `AuthStamp` without
18
18
  * the fingerprint — the guard computes that itself from the credential.
19
+ * `memory` is a store held in process (a session's own credentials), distinct
20
+ * from the on-disk `file` and the `exec` helper.
19
21
  */
20
22
  export interface CredentialOrigin {
21
- source: "file" | "exec";
23
+ source: "file" | "exec" | "memory";
22
24
  account?: string;
23
25
  }
24
26
  export interface ChangeGuardOptions {
25
27
  /** `warn` adopts the new identity and serves it; `refuse` throws instead. */
26
28
  onChange: CredentialAuthConfig["onChange"];
29
+ /**
30
+ * Fingerprint key. Absent = the machine salt file (today). A session store
31
+ * passes an in-memory salt, so its fingerprints never touch the slot.
32
+ */
33
+ readonly salt?: Buffer;
27
34
  /**
28
35
  * Where the credential read from `inner` came from. Called immediately after
29
36
  * each successful read, because the credential itself carries no provenance.
@@ -87,7 +87,7 @@ export function createChangeGuard(inner, options) {
87
87
  const described = options.describe(providerId);
88
88
  const identity = {
89
89
  kind: credential.kind,
90
- fingerprint: await fingerprintCredential(credential),
90
+ fingerprint: await fingerprintCredential(credential, options.salt),
91
91
  source: described?.source ?? "file",
92
92
  ...(described?.account !== undefined ? { account: described.account } : {}),
93
93
  };
@@ -22,5 +22,8 @@ export declare function _resetFingerprintSalt(): void;
22
22
  * is fingerprinted as the template, because nax-ai documents `key` as opaque.
23
23
  * `oauth` fingerprints its `refresh` token, so access-token rotation is
24
24
  * invisible while refresh-token rotation is not.
25
+ *
26
+ * `salt` overrides the machine salt file: an in-process caller that keeps its own
27
+ * key (a session store) passes one, and a supplied salt never touches the slot.
25
28
  */
26
- export declare function fingerprintCredential(credential: StoredCredential): Promise<string>;
29
+ export declare function fingerprintCredential(credential: StoredCredential, salt?: Buffer): Promise<string>;
@@ -166,9 +166,12 @@ function identifyingSecret(credential) {
166
166
  * is fingerprinted as the template, because nax-ai documents `key` as opaque.
167
167
  * `oauth` fingerprints its `refresh` token, so access-token rotation is
168
168
  * invisible while refresh-token rotation is not.
169
+ *
170
+ * `salt` overrides the machine salt file: an in-process caller that keeps its own
171
+ * key (a session store) passes one, and a supplied salt never touches the slot.
169
172
  */
170
- export async function fingerprintCredential(credential) {
171
- const salt = await resolveSalt();
173
+ export async function fingerprintCredential(credential, salt) {
174
+ const key = salt ?? (await resolveSalt());
172
175
  const secret = identifyingSecret(credential);
173
- return createHmac("sha256", salt).update(secret).digest("hex").slice(0, FINGERPRINT_CHARS);
176
+ return createHmac("sha256", key).update(secret).digest("hex").slice(0, FINGERPRINT_CHARS);
174
177
  }
@@ -0,0 +1,12 @@
1
+ import { type StoredCredential } from "@nathapp/nax-ai";
2
+ import { type GuardedCredentialStore } from "./change-guard.ts";
3
+ export type CredentialSource = {
4
+ readonly kind: "memory";
5
+ readonly credentials: Readonly<Record<string, StoredCredential>>;
6
+ } | {
7
+ readonly kind: "exec";
8
+ readonly command: readonly string[];
9
+ readonly timeoutMs?: number;
10
+ readonly onChange?: "warn" | "refuse";
11
+ };
12
+ export declare function createSessionCredentialStore(source: CredentialSource): GuardedCredentialStore;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A credential source owned by one session (S3 spec 5.2), instead of the
3
+ * process-wide store behind `configureCredentials`. It never reads the slot:
4
+ * the fingerprint salt is random per store, so an embedder that never called
5
+ * `configureCredentials` can still run a session and stamp its cost rows.
6
+ */
7
+ import { randomBytes } from "node:crypto";
8
+ import { createMemoryCredentialStore } from "@nathapp/nax-ai";
9
+ import { createChangeGuard } from "./change-guard.js";
10
+ import { createExecCredentialSource } from "./exec-source.js";
11
+ const SALT_BYTES = 32;
12
+ export function createSessionCredentialStore(source) {
13
+ const salt = randomBytes(SALT_BYTES);
14
+ if (source.kind === "memory") {
15
+ return createChangeGuard(createMemoryCredentialStore(source.credentials), {
16
+ onChange: "warn",
17
+ salt,
18
+ describe: () => ({ source: "memory" }),
19
+ });
20
+ }
21
+ const exec = createExecCredentialSource({
22
+ command: source.command,
23
+ ...(source.timeoutMs !== undefined ? { timeoutMs: source.timeoutMs } : {}),
24
+ });
25
+ return createChangeGuard(exec, {
26
+ onChange: source.onChange ?? "warn",
27
+ salt,
28
+ describe: (providerId) => {
29
+ const account = exec.accountOf(providerId);
30
+ return account === undefined ? { source: "exec" } : { source: "exec", account };
31
+ },
32
+ });
33
+ }
@@ -19,7 +19,9 @@ export { credentialFilePath, naxCredentialStore, type StoredEntry, servedAuth }
19
19
  export { NativeSessionUnsupportedError } from "./errors.ts";
20
20
  export { type ResolveResult, type ResolveStatus, resolveNativeId, } from "./model-resolver.ts";
21
21
  export { NATIVE_AGENT } from "./models.ts";
22
- export { MAX_RETAINED_TRANSCRIPTS, pruneRetainedTranscripts, } from "./session/transcript-store.ts";
22
+ export { createMemoryTranscriptStore, type MemoryTranscriptStore } from "./session/memory-transcript-store.ts";
23
+ export { createFileTranscriptStore, MAX_RETAINED_TRANSCRIPTS, pruneRetainedTranscripts, } from "./session/transcript-store.ts";
24
+ export type { TranscriptAcpRecord, TranscriptDoc, TranscriptStore, TurnMarker } from "./session/transcript-types.ts";
23
25
  export { NativeSessionAdapter } from "./session-adapter.ts";
24
26
  export { newSessionKey } from "./session-affinity.ts";
25
27
  export { type NativeTierConfig, nativeTierProviders } from "./tier-providers.ts";
@@ -17,7 +17,8 @@ export { credentialFilePath, naxCredentialStore, servedAuth } from "./credential
17
17
  export { NativeSessionUnsupportedError } from "./errors.js";
18
18
  export { resolveNativeId, } from "./model-resolver.js";
19
19
  export { NATIVE_AGENT } from "./models.js";
20
- export { MAX_RETAINED_TRANSCRIPTS, pruneRetainedTranscripts, } from "./session/transcript-store.js";
20
+ export { createMemoryTranscriptStore } from "./session/memory-transcript-store.js";
21
+ export { createFileTranscriptStore, MAX_RETAINED_TRANSCRIPTS, pruneRetainedTranscripts, } from "./session/transcript-store.js";
21
22
  export { NativeSessionAdapter } from "./session-adapter.js";
22
23
  export { newSessionKey } from "./session-affinity.js";
23
24
  export { nativeTierProviders } from "./tier-providers.js";
@@ -159,7 +159,7 @@ export interface BeforeCompactionPatch {
159
159
  /** A replacement summary, skipping the summarizer call. */
160
160
  readonly summary?: string;
161
161
  }
162
- /** `before_turn_end` — fires before the final saveTranscript. */
162
+ /** `before_turn_end` — fires before the turn-end transcript save. */
163
163
  export interface BeforeTurnEndPayload {
164
164
  readonly messages: readonly NativeTranscriptMessage[];
165
165
  readonly roundTrips: number;
@@ -33,6 +33,7 @@
33
33
  import { type SpinBreaker } from "#src/infra/spin-breaker/index";
34
34
  import { type InvalidCallBudget } from "./handle-invalid-tool-call.ts";
35
35
  import { type LoopEventRegistry, type LoopHandlerContext, type LoopHandlerSet } from "./loop-events/index.ts";
36
+ import type { NativeSessionState } from "./session.ts";
36
37
  export interface BuiltinLoopHandlerDeps {
37
38
  /**
38
39
  * The session the handlers run for. Truncation resolves its spill root
@@ -40,6 +41,8 @@ export interface BuiltinLoopHandlerDeps {
40
41
  * per-turn dep, so a repointed registry never keeps a stale session.
41
42
  */
42
43
  readonly sessionName: string;
44
+ /** The owning adapter's per-session state, read through `state.current` at dispatch like the session name. */
45
+ readonly sessionState: NativeSessionState;
43
46
  /** Per-turn invalid-call budget. The loop reads `exceeded` for the halt. */
44
47
  readonly budget: InvalidCallBudget;
45
48
  /** Absent disables the breaker, exactly as `TurnDeps.spinBreaker` does. */
@@ -96,7 +96,7 @@ export function registerBuiltinLoopHandlers(registry, deps) {
96
96
  // whatever the handlers produced. The handler is rebuilt per dispatch so the
97
97
  // session name is read through `state.current` — the same lifetime the other
98
98
  // per-turn deps use — rather than frozen at install time.
99
- registry.register("after_tool", (payload) => createTruncationHandler(state.current.sessionName)(payload));
99
+ registry.register("after_tool", (payload) => createTruncationHandler(state.current.sessionState, state.current.sessionName)(payload));
100
100
  }
101
101
  function repairInvalidCall(state, call, tools) {
102
102
  const invalid = state.current.budget.observe(call, tools);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * An in-process `TranscriptStore` for tests and simple embedders (S3 spec 5.5).
3
+ * Every document crossing the boundary is copied, so a caller mutating what it
4
+ * saved or loaded cannot rewrite stored history. History is lost with the
5
+ * process; an embedder that must resume after a restart injects a durable store.
6
+ */
7
+ import type { TranscriptDoc, TranscriptStore } from "./transcript-types.ts";
8
+ export interface MemoryTranscriptStore extends TranscriptStore {
9
+ /** The last document `retainFailed` moved aside for `sessionId`, if any. */
10
+ retained(sessionId: string): TranscriptDoc | undefined;
11
+ }
12
+ export declare function createMemoryTranscriptStore(): MemoryTranscriptStore;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * An in-process `TranscriptStore` for tests and simple embedders (S3 spec 5.5).
3
+ * Every document crossing the boundary is copied, so a caller mutating what it
4
+ * saved or loaded cannot rewrite stored history. History is lost with the
5
+ * process; an embedder that must resume after a restart injects a durable store.
6
+ */
7
+ export function createMemoryTranscriptStore() {
8
+ const live = new Map();
9
+ const failed = new Map();
10
+ const copy = (doc) => doc === undefined ? undefined : structuredClone(doc);
11
+ return {
12
+ load: (sessionId) => Promise.resolve(copy(live.get(sessionId)) ?? null),
13
+ save: (sessionId, doc) => {
14
+ live.set(sessionId, structuredClone(doc));
15
+ return Promise.resolve();
16
+ },
17
+ retainFailed: (sessionId) => {
18
+ const doc = live.get(sessionId);
19
+ if (doc !== undefined) {
20
+ failed.set(sessionId, doc);
21
+ live.delete(sessionId);
22
+ }
23
+ return Promise.resolve();
24
+ },
25
+ delete: (sessionId) => {
26
+ live.delete(sessionId);
27
+ return Promise.resolve();
28
+ },
29
+ markTurn: (sessionId, marker) => {
30
+ const base = live.get(sessionId) ?? { savedAt: new Date().toISOString(), messages: [] };
31
+ live.set(sessionId, { ...base, turn: { ...marker } });
32
+ return Promise.resolve();
33
+ },
34
+ retained: (sessionId) => copy(failed.get(sessionId)),
35
+ };
36
+ }
@@ -10,104 +10,133 @@ import { type SpinBreaker } from "#src/infra/spin-breaker/index";
10
10
  import type { AgentStreamEvent } from "#src/session/agent-stream-events";
11
11
  import type { OpenSessionOpts, SessionHandle } from "#src/session/session-types";
12
12
  import type { ResolvedCompaction } from "./compaction.ts";
13
+ import type { TranscriptStore } from "./transcript-types.ts";
13
14
  import type { TurnRetryConfig } from "./turn-retry.ts";
14
- /**
15
- * Session name -> transcript directory, so sendTurn and close can find it.
16
- *
17
- * Cleared by every close path — `closeNativeSession` (in-process) and the
18
- * adapter's `closePhysicalSession` (run teardown, which only has the name) —
19
- * through the shared `clearNativeSessionState`. A caller that opens a session
20
- * and never closes it (e.g. an early return, or a thrown error between open and
21
- * close) still leaks an entry for the lifetime of the process. Harmless in
22
- * practice (it is a small in-memory map keyed by session name, not a handle
23
- * to a real resource), but worth knowing when debugging a growing map.
24
- */
25
- export declare const nativeTranscriptDirs: Map<string, string>;
26
- /**
27
- * Session name -> the root whose `.nax/scratchpad/` holds this session's
28
- * spill files (US-003).
29
- *
30
- * Recorded from the session's `workdir` at open — the same root the coding
31
- * tools are confined to — so a spilled body is reachable through
32
- * `ScratchpadRead` with the relative path the truncation marker names. Same
33
- * lifecycle as the maps above: set on open, cleared on close.
34
- */
35
- export declare const nativeSessionScratchpadRoots: Map<string, string>;
36
- /**
37
- * Session name -> timeoutSeconds, so sendTurn can bound each `complete()`
38
- * call with a deadline (whole-branch review finding 4). Same lifecycle as
39
- * `nativeTranscriptDirs` — populated on open, cleared on close only.
40
- */
41
- export declare const nativeSessionTimeouts: Map<string, number>;
42
- /**
43
- * Session name -> the runtime hooks handed to openSession. SessionManager
44
- * passes both (manager.ts, openSession) and the native adapter previously
45
- * ignored them, which is why the idle watchdog never covered native. Same
46
- * lifecycle as `nativeTranscriptDirs`: set on open, cleared on close only.
47
- */
48
- export declare const nativeSessionStreamHooks: Map<string, {
15
+ /** The runtime hooks handed to openSession. */
16
+ export interface NativeSessionStreamHooks {
49
17
  onStreamActivity?: (event: AgentStreamEvent) => void;
50
18
  onActiveCall?: (callId: string, cancel: () => Promise<void>) => void;
51
- }>;
52
- /**
53
- * Session names whose most recent turn failed.
54
- *
55
- * `AgentAdapter.closeSession(handle)` carries no failure signal, so the close
56
- * cannot tell a finished session from a broken one and passed `failed: false`
57
- * for both -- deleting the transcript of exactly the session whose history the
58
- * retry needs and a human would read (nax#1838). Rather than widen the adapter
59
- * interface for one transport, the turn records what it knows here and the
60
- * close reads it. Same lifecycle as the maps above: written per turn, cleared
61
- * on close.
62
- *
63
- * A later successful turn clears the mark. "Failed" describes the session's
64
- * current state, not whether it ever stumbled -- a session that recovered and
65
- * finished is still cleaned up.
66
- */
67
- export declare const nativeSessionFailed: Set<string>;
68
- /**
69
- * Session name -> the identity that owns this session's transcript (nax#1877).
70
- *
71
- * Read by `runNativeTurn` on both the load and the save, so a transcript left
72
- * behind by an abandoned invocation of the same deterministic session name is
73
- * recognised as foreign rather than silently resumed. Same lifecycle as the
74
- * maps above: set on open, cleared on close.
75
- */
76
- export declare const nativeSessionTranscriptOwners: Map<string, string>;
77
- /** Session name -> resolved compaction settings. Same lifecycle as the maps above. */
78
- export declare const nativeSessionCompaction: Map<string, ResolvedCompaction>;
79
- /**
80
- * Session name -> resolved transport-fault retry settings (nax#1870). Same
81
- * lifecycle as the maps above: set on open, cleared on close.
82
- */
83
- export declare const nativeSessionTransportRetry: Map<string, TurnRetryConfig>;
84
- /**
85
- * Session name -> live `SpinBreaker` instance (nax#2013, lifetime extended in
86
- * nax#2047). Constructed once at open so the cumulative counter survives
87
- * across `runNativeTurn` calls — a fix round that opens a new turn on the
88
- * same `handle.id` must inherit the prior turn's evidence rather than start
89
- * fresh. Same lifecycle as `nativeSessionTransportRetry`: set on open, cleared
90
- * on close.
91
- */
92
- export declare const nativeSessionSpinBreaker: Map<string, SpinBreaker>;
19
+ }
20
+ /** Where one open session's history lives, and whether close may remove it. */
21
+ export interface SessionTranscript {
22
+ readonly store: TranscriptStore;
23
+ readonly retainOnClose: boolean;
24
+ }
93
25
  /**
94
- * Session name -> the last round trip's reported input tokens and the index it
95
- * covers, so the next estimate can anchor on a real number.
96
- *
97
- * In-memory rather than persisted because runNativeTurn reloads the transcript
98
- * from disk on EVERY turn: without this the second turn of every session would
99
- * be estimated from scratch. A process restart still loses it, which is the case
100
- * the reactive backstop covers.
26
+ * The per-owner state behind the native session lifecycle.
101
27
  *
102
- * `model` is the transcript model identity the anchor was measured under
103
- * (P3 spec 8.3(d)); read it through `sessionAnchorFor`, never directly.
28
+ * Every map below is keyed by session name and owned by one state instance, so
29
+ * two owners (the orchestrator and a test, or two concurrent runs) never share
30
+ * an entry for the same deterministic name.
104
31
  */
32
+ export interface NativeSessionState {
33
+ /**
34
+ * Session name -> transcript directory, so sendTurn and close can find it.
35
+ *
36
+ * Cleared by every close path — `closeNativeSession` (in-process) and the
37
+ * adapter's `closePhysicalSession` (run teardown, which only has the name) —
38
+ * through the shared `clearNativeSessionState`. A caller that opens a session
39
+ * and never closes it (e.g. an early return, or a thrown error between open and
40
+ * close) still leaks an entry for the lifetime of the state. Harmless in
41
+ * practice (it is a small in-memory map keyed by session name, not a handle
42
+ * to a real resource), but worth knowing when debugging a growing map.
43
+ */
44
+ readonly transcriptDirs: Map<string, string>;
45
+ /**
46
+ * Session name -> the session's transcript store and close policy (S3-1).
47
+ * Set by every `openNativeSession`; read through `sessionTranscriptFor`. Same
48
+ * lifecycle as the maps above: set on open, cleared on close.
49
+ */
50
+ readonly transcripts: Map<string, SessionTranscript>;
51
+ /**
52
+ * Session name -> the root whose `.nax/scratchpad/` holds this session's
53
+ * spill files (US-003).
54
+ *
55
+ * Recorded from the session's `workdir` at open — the same root the coding
56
+ * tools are confined to — so a spilled body is reachable through
57
+ * `ScratchpadRead` with the relative path the truncation marker names. Same
58
+ * lifecycle as the maps above: set on open, cleared on close.
59
+ */
60
+ readonly scratchpadRoots: Map<string, string>;
61
+ /**
62
+ * Session name -> timeoutSeconds, so sendTurn can bound each `complete()`
63
+ * call with a deadline (whole-branch review finding 4). Same lifecycle as
64
+ * `transcriptDirs` — populated on open, cleared on close only.
65
+ */
66
+ readonly timeouts: Map<string, number>;
67
+ /**
68
+ * Session name -> the runtime hooks handed to openSession. SessionManager
69
+ * passes both (manager.ts, openSession) and the native adapter previously
70
+ * ignored them, which is why the idle watchdog never covered native. Same
71
+ * lifecycle as `transcriptDirs`: set on open, cleared on close only.
72
+ */
73
+ readonly streamHooks: Map<string, NativeSessionStreamHooks>;
74
+ /**
75
+ * Session names whose most recent turn failed.
76
+ *
77
+ * `AgentAdapter.closeSession(handle)` carries no failure signal, so the close
78
+ * cannot tell a finished session from a broken one and passed `failed: false`
79
+ * for both -- deleting the transcript of exactly the session whose history the
80
+ * retry needs and a human would read (nax#1838). Rather than widen the adapter
81
+ * interface for one transport, the turn records what it knows here and the
82
+ * close reads it. Same lifecycle as the maps above: written per turn, cleared
83
+ * on close.
84
+ *
85
+ * A later successful turn clears the mark. "Failed" describes the session's
86
+ * current state, not whether it ever stumbled -- a session that recovered and
87
+ * finished is still cleaned up.
88
+ */
89
+ readonly failed: Set<string>;
90
+ /**
91
+ * Session name -> the identity that owns this session's transcript (nax#1877).
92
+ *
93
+ * Read by `runNativeTurn` on both the load and the save, so a transcript left
94
+ * behind by an abandoned invocation of the same deterministic session name is
95
+ * recognised as foreign rather than silently resumed. Same lifecycle as the
96
+ * maps above: set on open, cleared on close.
97
+ */
98
+ readonly transcriptOwners: Map<string, string>;
99
+ /** Session name -> resolved compaction settings. Same lifecycle as the maps above. */
100
+ readonly compaction: Map<string, ResolvedCompaction>;
101
+ /**
102
+ * Session name -> resolved transport-fault retry settings (nax#1870). Same
103
+ * lifecycle as the maps above: set on open, cleared on close.
104
+ */
105
+ readonly transportRetry: Map<string, TurnRetryConfig>;
106
+ /**
107
+ * Session name -> live `SpinBreaker` instance (nax#2013, lifetime extended in
108
+ * nax#2047). Constructed once at open so the cumulative counter survives
109
+ * across `runNativeTurn` calls — a fix round that opens a new turn on the
110
+ * same `handle.id` must inherit the prior turn's evidence rather than start
111
+ * fresh. Same lifecycle as `transportRetry`: set on open, cleared on close.
112
+ */
113
+ readonly spinBreakers: Map<string, SpinBreaker>;
114
+ /**
115
+ * Session name -> the last round trip's reported input tokens and the index it
116
+ * covers, so the next estimate can anchor on a real number.
117
+ *
118
+ * In-memory rather than persisted because runNativeTurn reloads the transcript
119
+ * from disk on EVERY turn: without this the second turn of every session would
120
+ * be estimated from scratch. A process restart still loses it, which is the case
121
+ * the reactive backstop covers.
122
+ *
123
+ * `model` is the transcript model identity the anchor was measured under
124
+ * (P3 spec 8.3(d)); read it through `sessionAnchorFor`, never directly.
125
+ */
126
+ readonly lastUsage: Map<string, SessionAnchor>;
127
+ /**
128
+ * Session name -> the system prompt sent as each round trip's top-level
129
+ * `system` (S3-4; the facade's `instructions`). nax opens without one. Same
130
+ * lifecycle as the maps above: set on open, cleared on close.
131
+ */
132
+ readonly systemPrompts: Map<string, string>;
133
+ }
134
+ export declare function createNativeSessionState(): NativeSessionState;
105
135
  export interface SessionAnchor {
106
136
  readonly promptTokens: number;
107
137
  readonly anchorIndex: number;
108
138
  readonly model?: string;
109
139
  }
110
- export declare const nativeSessionLastUsage: Map<string, SessionAnchor>;
111
140
  /**
112
141
  * The persisted anchor a turn on `model` may use. Prefix stability is a
113
142
  * property of (model, prefix) (P3 spec 3.3): an anchor measured under a
@@ -115,12 +144,23 @@ export declare const nativeSessionLastUsage: Map<string, SessionAnchor>;
115
144
  * so it is dropped here rather than mis-sizing the next compaction decision.
116
145
  * An entry or a turn with no model makes no claim — the store's own rule.
117
146
  */
118
- export declare function sessionAnchorFor(sessionName: string, model: string | undefined): SessionAnchor | undefined;
147
+ export declare function sessionAnchorFor(state: NativeSessionState, sessionName: string, model: string | undefined): SessionAnchor | undefined;
119
148
  /** Records how the session's latest turn ended, for `closeNativeSession`. */
120
- export declare function markNativeTurnOutcome(sessionName: string, failed: boolean): void;
121
- export declare function openNativeSession(name: string, opts: OpenSessionOpts): Promise<SessionHandle>;
149
+ export declare function markNativeTurnOutcome(state: NativeSessionState, sessionName: string, failed: boolean): void;
150
+ /**
151
+ * The session's transcript. A state seeded with only a transcript directory
152
+ * (tests that drive `runNativeTurn` without opening) resolves to the file store
153
+ * over it; `openNativeSession` always records an entry, so production does not
154
+ * take that path.
155
+ */
156
+ export declare function sessionTranscriptFor(state: NativeSessionState, sessionName: string): SessionTranscript | undefined;
157
+ /** The request field for the session's system prompt: `{ system }`, or `{}` when it has none. */
158
+ export declare function systemFieldFor(state: NativeSessionState, sessionName: string): {
159
+ readonly system?: string;
160
+ };
161
+ export declare function openNativeSession(state: NativeSessionState, name: string, opts: OpenSessionOpts): Promise<SessionHandle>;
122
162
  /**
123
- * Clear every module-level entry for one session name.
163
+ * Clear every state entry for one session name.
124
164
  *
125
165
  * Extracted so both close paths share one source of truth. `closeNativeSession`
126
166
  * has a `SessionHandle`; the adapter's `closePhysicalSession` gets only the
@@ -129,11 +169,13 @@ export declare function openNativeSession(name: string, opts: OpenSessionOpts):
129
169
  * is that name — but the signatures are not interchangeable, so this helper is
130
170
  * string-keyed rather than reconstructed into a synthetic handle.
131
171
  */
132
- export declare function clearNativeSessionState(sessionName: string): void;
172
+ export declare function clearNativeSessionState(state: NativeSessionState, sessionName: string): void;
133
173
  /**
134
174
  * Kept on failure, deleted on success. Every Phase B op is lifetime "fresh", so
135
- * the transcript survives exactly when it is worth reading. The kept-on-failure
136
- * set is otherwise unbounded, so a failed close also prunes the feature's
137
- * `sessions/` directory down to `MAX_RETAINED_TRANSCRIPTS` (ADR-028 section 3).
175
+ * the transcript survives exactly when it is worth reading. Keeping is the
176
+ * store's `retainFailed`; the file store's also prunes the feature's
177
+ * `sessions/` directory down to `MAX_RETAINED_TRANSCRIPTS` (ADR-028 section 3),
178
+ * since the kept-on-failure set is otherwise unbounded. A session opened with
179
+ * `retainOnClose` skips both: its live document stays resumable.
138
180
  */
139
- export declare function closeNativeSession(handle: SessionHandle, failed?: boolean): Promise<void>;
181
+ export declare function closeNativeSession(state: NativeSessionState, handle: SessionHandle, failed?: boolean): Promise<void>;