@gajae-code/coding-agent 0.5.2 → 0.5.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/async/job-manager.d.ts +6 -0
  3. package/dist/types/config/model-profiles.d.ts +10 -0
  4. package/dist/types/dap/client.d.ts +2 -1
  5. package/dist/types/edit/read-file.d.ts +6 -0
  6. package/dist/types/eval/js/context-manager.d.ts +3 -0
  7. package/dist/types/eval/js/executor.d.ts +1 -0
  8. package/dist/types/exec/bash-executor.d.ts +2 -0
  9. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  10. package/dist/types/lsp/types.d.ts +2 -0
  11. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  12. package/dist/types/modes/components/model-selector.d.ts +2 -0
  13. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  14. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  16. package/dist/types/modes/interactive-mode.d.ts +1 -0
  17. package/dist/types/modes/types.d.ts +1 -0
  18. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  19. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  20. package/dist/types/runtime-mcp/types.d.ts +2 -0
  21. package/dist/types/session/agent-session.d.ts +29 -1
  22. package/dist/types/session/artifacts.d.ts +4 -1
  23. package/dist/types/session/streaming-output.d.ts +12 -0
  24. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  25. package/dist/types/tools/bash.d.ts +1 -0
  26. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  27. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  28. package/dist/types/web/search/providers/codex.d.ts +4 -4
  29. package/package.json +7 -7
  30. package/src/async/job-manager.ts +181 -43
  31. package/src/config/file-lock.ts +9 -1
  32. package/src/config/model-profile-activation.ts +71 -3
  33. package/src/config/model-profiles.ts +39 -14
  34. package/src/dap/client.ts +105 -64
  35. package/src/dap/session.ts +44 -7
  36. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
  37. package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
  38. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
  39. package/src/edit/read-file.ts +19 -1
  40. package/src/eval/js/context-manager.ts +228 -65
  41. package/src/eval/js/executor.ts +2 -0
  42. package/src/eval/js/index.ts +1 -0
  43. package/src/eval/js/worker-core.ts +10 -6
  44. package/src/eval/py/executor.ts +68 -19
  45. package/src/eval/py/kernel.ts +46 -22
  46. package/src/eval/py/runner.py +68 -14
  47. package/src/exec/bash-executor.ts +49 -13
  48. package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
  49. package/src/gjc-runtime/ralplan-runtime.ts +10 -0
  50. package/src/gjc-runtime/state-runtime.ts +73 -0
  51. package/src/gjc-runtime/tmux-gc.ts +86 -37
  52. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  53. package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
  54. package/src/internal-urls/artifact-protocol.ts +10 -1
  55. package/src/internal-urls/docs-index.generated.ts +2 -2
  56. package/src/lsp/client.ts +64 -26
  57. package/src/lsp/index.ts +2 -1
  58. package/src/lsp/lspmux.ts +33 -9
  59. package/src/lsp/types.ts +2 -0
  60. package/src/modes/bridge/bridge-mode.ts +21 -0
  61. package/src/modes/components/assistant-message.ts +10 -2
  62. package/src/modes/components/bash-execution.ts +5 -1
  63. package/src/modes/components/eval-execution.ts +5 -1
  64. package/src/modes/components/model-selector.ts +34 -2
  65. package/src/modes/components/oauth-selector.ts +5 -0
  66. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  67. package/src/modes/components/skill-message.ts +24 -16
  68. package/src/modes/components/tool-execution.ts +6 -0
  69. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  70. package/src/modes/controllers/input-controller.ts +19 -0
  71. package/src/modes/controllers/selector-controller.ts +6 -1
  72. package/src/modes/interactive-mode.ts +13 -0
  73. package/src/modes/types.ts +1 -0
  74. package/src/modes/utils/ui-helpers.ts +5 -2
  75. package/src/prompts/agents/executor.md +1 -1
  76. package/src/runtime/process-lifecycle.ts +400 -0
  77. package/src/runtime-mcp/manager.ts +164 -50
  78. package/src/runtime-mcp/transports/http.ts +12 -11
  79. package/src/runtime-mcp/transports/stdio.ts +64 -38
  80. package/src/runtime-mcp/types.ts +3 -0
  81. package/src/sdk.ts +27 -0
  82. package/src/session/agent-session.ts +271 -25
  83. package/src/session/artifacts.ts +17 -2
  84. package/src/session/blob-store.ts +36 -2
  85. package/src/session/session-manager.ts +29 -13
  86. package/src/session/streaming-output.ts +95 -3
  87. package/src/setup/model-onboarding-guidance.ts +10 -3
  88. package/src/skill-state/active-state.ts +79 -7
  89. package/src/slash-commands/builtin-registry.ts +30 -3
  90. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  91. package/src/tools/archive-reader.ts +10 -1
  92. package/src/tools/bash.ts +11 -4
  93. package/src/tools/browser/registry.ts +17 -1
  94. package/src/tools/browser/tab-supervisor.ts +22 -0
  95. package/src/tools/browser.ts +38 -4
  96. package/src/tools/cron.ts +2 -6
  97. package/src/tools/read.ts +11 -12
  98. package/src/tools/sqlite-reader.ts +19 -5
  99. package/src/web/search/providers/codex.ts +6 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.5.4] - 2026-06-17
6
+
7
+ ### Fixed
8
+
9
+ - Fixed subagent resume returning `not_found` after terminal job eviction removed the in-memory subagent record. Resume descriptors are now retained as durable same-session metadata and rehydrate a resumable record from the saved subagent session file, so ralplan Planner revision passes can resume with fallback metadata instead of forcing a fresh Planner spawn after 0.5.3.
10
+ - `AgentSession` now forwards the live provider session state (`providerSessionState`), session affinity id (`providerSessionId ?? sessionId`), and configured WebSocket transport preference (`preferWebsockets`) into local maintenance one-shot calls — manual/automatic compaction summaries, handoff generation, and tree branch summaries — via a shared `#maintenanceProviderTransport()` helper. Previously these Codex/OpenAI-compatible maintenance calls could fall back to HTTP/SSE and lose `session_id` affinity even when `providers.openaiWebsockets: "on"` routed live turns over WebSocket (#736).
11
+ - Fixed `ollama-cloud` first-event timeouts driving an unbounded, usage-spiking retry loop. The ollama-chat backend (exclusively `ollama-cloud`; local Ollama uses the `openai-responses` API) can stall before its first token even for tiny prompts, surfacing `Provider stream timed out while waiting for the first event`. That message matched the generic transient classifier, which retries forever (capped only on delay), so every continuation re-issued the full request to a billable backend and silently spiked usage — disabling retries was the only workaround. First-event timeouts on the ollama-chat API are now a distinct fail-closed class bounded by `retry.maxRetries`: they still retry transient cold starts a few times, then surface instead of looping. First-party providers keep their existing unbounded first-event-timeout retry behavior (#713).
12
+ - Interactive sessions no longer orphan the `browser` tool's headless/spawned Chrome (and the Python eval kernel) to PID 1 when killed by a signal. The interactive entry now registers a bounded, idempotent `postmortem` cleanup (`session-subprocess-teardown`) that runs `AgentSession.disposeChildSubprocesses()` on `SIGINT`/`SIGTERM`/`SIGHUP`, force-releasing the session's browser tabs (`kill:true`) and disposing its Python/JS kernels — the teardown the graceful `/quit` (`dispose()`) path already performs but that an external `kill`/terminal-close used to bypass. Headless `disposeBrowserHandle` now also SIGTERM/SIGKILLs the captured Chrome process tree as a fallback when forced, so a wedged renderer can't survive a bounded CDP `close()`; graceful release behavior is unchanged. The teardown is time-boxed (5s) so a stuck subprocess can't hang process exit (#698).
13
+
14
+ ## [0.5.3] - 2026-06-16
15
+
16
+ ### Added
17
+
18
+ - Added `GJC_CREDENTIAL_RANKING_MODE` env var (`balanced` (default) | `earliest-reset`), wired through `discoverAuthStorage` into `AuthStorage.credentialRankingMode`. `earliest-reset` selects multi-account OAuth credentials earliest-expiry-first so soon-to-reset tumbling-window quota (e.g. Claude 5h/7d) is drained before it is lost at reset; unset/unknown leaves the default `balanced` behavior unchanged.
19
+ - The `/model` selector and `/fast` status now show a per-model fast-mode indicator (`⚡`) resolved with the provider-aware predicate, including subagent (role) models evaluated against the effective `task.serviceTier` (falling back to the session tier when `inherit`), so it is visible at a glance which models will run with Anthropic `speed:"fast"` / OpenAI `service_tier=priority`. Display-only: no `serviceTier`/`modelRoles`/`agentModelOverrides` writes (#691).
20
+ - Added an opt-in `GJC_BRIDGE_ENDPOINTS` env var (`all`, or a comma list of matrix keys: `events`, `commands`, `control`, `uiResponses`, `hostToolResults`, `hostUriResults`) to enable bridge-mode session-control endpoints, which were previously permanently fail-closed (`403 endpoint_disabled`) with no supported way to turn them on. Unset stays fail-closed and backward compatible (#663).
21
+
22
+ ### Fixed
23
+
24
+ - Auto-compaction no longer silently requires OpenAI when the active route is a custom Anthropic-capable provider. The compaction model-candidate selection already prefers the active session model, but its last-resort "largest-context model" fallback scanned the entire bundled catalog across all providers, so a stray OpenAI credential (e.g. an out-of-credit key left in the environment) could be picked when the active provider's compaction credential was unusable — turning OpenAI into an implicit hard dependency. The implicit fallback is now scoped to the active model's provider; cross-provider compaction still works but only when explicitly configured via `modelRoles`. When the active provider cannot compact and no role is configured, compaction now fails with the existing clear, provider-specific credential error instead of reaching for OpenAI (#697).
25
+ - Long-running-session freeze/leak remediation across the TUI, agent, and tool runtime: the TUI render loop, component-dispose lifecycle, and markdown highlighting are hardened against huge frames and reuse leaks (#716); agent context append, an emergency-compaction floor that cannot be disabled, token accounting, and session resource teardown (own-session browser tabs, LSP clients, Cursor conversation cache) are bounded (#717); oversized tool inputs/outputs are capped (8 MiB edit/read guard ahead of the notebook fast-path, 1000-row SQLite raw-query cap, 16 MiB artifact / 256 MiB archive read caps, budget-bounded browser return serialization) (#721); native synchronous entrypoints add defense-in-depth caps for tokenization, highlighting, and fuzzy edit matching (#744); and the session blob store is LRU-bounded (64 MiB / 4096 entries) with bounded-concurrency blob resume (limit 8) (#719).
26
+ - Process & resource lifecycle hardening so child processes and external resources are reliably reaped on disconnect, abort, and shutdown, built on a new owned-process foundation — process-group ownership with escalating SIGTERM→SIGKILL tree termination, idempotent dispose, and a postmortem reap hook (F1). Owned-process handles are terminalized on clean drain so a retained handle can never signal a recycled PID/process group (B1); the native blocking-task boundary, PTY lifecycle, and pi-shell timeout/abort reaping are hardened (U1–U3); the Python eval kernel (U4) and JS eval worker/VM (U5) coalesce concurrent first cells, settle queued/pending runs on teardown, and return worker/kernel counts to baseline; bash shell sessions are owner-scoped with one-shot async/monitor jobs and a hard artifact byte cap (U6); DAP adapters and LSP servers are spawned as owned processes and killed on terminate/timeout/reload (U7); MCP stdio/HTTP/SSE transports and the manager close idempotently with stale-publication identity guards (U8); the async job-manager bounds dispose, the delivery queue/retry (with dead-lettering), and terminal purge (U9); and tmux GC never prunes live/attached sessions and reaps only durably-owned orphans (U10).
27
+
5
28
  ## [0.5.2] - 2026-06-15
6
29
 
7
30
  ### Fixed
@@ -109,11 +109,16 @@ export interface AsyncJobManagerOptions {
109
109
  maxRunningJobs?: number;
110
110
  retentionMs?: number;
111
111
  }
112
+ export interface AsyncJobDisposeDiagnostics {
113
+ stuckJobIds: string[];
114
+ deliveriesDrained: boolean;
115
+ }
112
116
  export interface AsyncJobDeliveryState {
113
117
  queued: number;
114
118
  delivering: boolean;
115
119
  nextRetryAt?: number;
116
120
  pendingJobIds: string[];
121
+ deadLettered: number;
117
122
  }
118
123
  export interface AsyncJobLifecycleCleanup {
119
124
  onCancel?: (job: AsyncJob) => void;
@@ -308,6 +313,7 @@ export declare class AsyncJobManager {
308
313
  * (used by `dispose()` to nuke the manager's state).
309
314
  */
310
315
  cancelAll(filter?: AsyncJobFilter): void;
316
+ getLastDisposeDiagnostics(): AsyncJobDisposeDiagnostics;
311
317
  waitForAll(): Promise<void>;
312
318
  drainDeliveries(options?: {
313
319
  timeoutMs?: number;
@@ -4,6 +4,16 @@ export type ModelProfileRole = GjcModelAssignmentTargetId;
4
4
  export interface ModelProfileDefinition {
5
5
  name: string;
6
6
  requiredProviders: string[];
7
+ /**
8
+ * Optional groups of providers that are interchangeable fallbacks.
9
+ * Each group is an array of provider ids where at least one must be
10
+ * authenticated. Providers NOT in any group are treated as strict
11
+ * requirements (all must be authenticated).
12
+ *
13
+ * Example: `[["xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn"]]`
14
+ * means any single xiaomi credential satisfies the group.
15
+ */
16
+ alternativeProviderGroups?: readonly (readonly string[])[];
7
17
  modelMapping: Partial<Record<ModelProfileRole, string>>;
8
18
  source: "builtin" | "user";
9
19
  }
@@ -1,3 +1,4 @@
1
+ import { type OwnedProcess } from "../runtime/process-lifecycle";
1
2
  import type { DapCapabilities, DapClientState, DapEventMessage, DapInitializeArguments, DapRequestMessage, DapResolvedAdapter } from "./types";
2
3
  interface DapSpawnOptions {
3
4
  adapter: DapResolvedAdapter;
@@ -15,7 +16,7 @@ export declare class DapClient {
15
16
  readonly adapter: DapResolvedAdapter;
16
17
  readonly cwd: string;
17
18
  readonly proc: DapClientState["proc"];
18
- constructor(adapter: DapResolvedAdapter, cwd: string, proc: DapClientState["proc"], options?: {
19
+ constructor(adapter: DapResolvedAdapter, cwd: string, owner: OwnedProcess, options?: {
19
20
  readable?: ReadableStream<Uint8Array>;
20
21
  writeSink?: DapWriteSink;
21
22
  socket?: {
@@ -1,2 +1,8 @@
1
+ /**
2
+ * Max byte size of a file the edit modes will load whole. Editing loads + normalizes +
3
+ * fuzzy-matches + diffs the entire file on the main thread, so a multi-MB/generated file
4
+ * would block the event loop (F19). Above this, fail fast with an actionable error.
5
+ */
6
+ export declare const MAX_EDIT_FILE_BYTES: number;
1
7
  export declare function readEditFileText(absolutePath: string, path: string): Promise<string>;
2
8
  export declare function serializeEditFileText(absolutePath: string, path: string, content: string): Promise<string>;
@@ -12,6 +12,7 @@ export declare function executeInVmContext(options: {
12
12
  sessionId: string;
13
13
  cwd: string;
14
14
  session: ToolSession;
15
+ ownerId?: string;
15
16
  reset?: boolean;
16
17
  code: string;
17
18
  filename: string;
@@ -21,4 +22,6 @@ export declare function executeInVmContext(options: {
21
22
  value: unknown;
22
23
  }>;
23
24
  export declare function resetVmContext(sessionKey: string): Promise<void>;
25
+ export declare function disposeVmContextsByOwner(ownerId: string): Promise<void>;
24
26
  export declare function disposeAllVmContexts(): Promise<void>;
27
+ export declare function liveVmContextCount(): number;
@@ -7,6 +7,7 @@ export interface JsExecutorOptions {
7
7
  onChunk?: (chunk: string) => Promise<void> | void;
8
8
  signal?: AbortSignal;
9
9
  sessionId: string;
10
+ ownerId?: string;
10
11
  reset?: boolean;
11
12
  sessionFile?: string;
12
13
  artifactPath?: string;
@@ -19,6 +19,8 @@ export interface BashExecutorOptions {
19
19
  /** Artifact path/id for full output storage */
20
20
  artifactPath?: string;
21
21
  artifactId?: string;
22
+ /** Execute without retaining a native Shell in the persistent session registry. */
23
+ oneShot?: boolean;
22
24
  /**
23
25
  * Invoked when the native minimizer rewrote the command's output, giving
24
26
  * the caller a chance to persist the lossless original capture (typically
@@ -8,15 +8,21 @@ export interface GjcTmuxSessionStatus {
8
8
  branch?: string;
9
9
  branchSlug?: string;
10
10
  project?: string;
11
+ panePids: number[];
12
+ profile?: string;
11
13
  }
12
14
  export interface GjcTmuxSessionTagsForGc {
13
15
  profile?: string;
14
16
  project?: string;
15
17
  branch?: string;
18
+ branchSlug?: string;
19
+ createdAt?: string;
20
+ attached?: boolean;
21
+ panePids?: number[];
16
22
  }
17
23
  export interface GjcTmuxSessionsForGc {
18
24
  tagged: GjcTmuxSessionStatus[];
19
- untagged: string[];
25
+ untagged: GjcTmuxSessionStatus[];
20
26
  }
21
27
  export declare function listGjcTmuxSessions(env?: NodeJS.ProcessEnv): GjcTmuxSessionStatus[];
22
28
  /** @internal */
@@ -1,5 +1,6 @@
1
1
  import type { ptree } from "@gajae-code/utils";
2
2
  import * as z from "zod/v4";
3
+ import type { OwnedProcess } from "../runtime/process-lifecycle";
3
4
  export declare const lspSchema: z.ZodObject<{
4
5
  action: z.ZodEnum<{
5
6
  capabilities: "capabilities";
@@ -261,6 +262,7 @@ export interface LspClient {
261
262
  cwd: string;
262
263
  config: ServerConfig;
263
264
  proc: ptree.ChildProcess<"pipe">;
265
+ owner?: OwnedProcess;
264
266
  requestId: number;
265
267
  diagnostics: Map<string, PublishedDiagnostics>;
266
268
  diagnosticsVersion: number;
@@ -41,6 +41,7 @@ interface BridgeIdempotencyRecord {
41
41
  pending?: boolean;
42
42
  }
43
43
  type BridgeIdempotencyCache = Map<string, BridgeIdempotencyRecord>;
44
+ export declare function parseBridgeEndpoints(value: string | undefined): Partial<BridgeEndpointMatrix> | undefined;
44
45
  export declare function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (request: Request) => Promise<Response>;
45
46
  export declare function runBridgeMode(session: AgentSession, setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void): Promise<never>;
46
47
  export {};
@@ -30,6 +30,8 @@ export declare class ModelSelectorComponent extends Container {
30
30
  temporaryOnly?: boolean;
31
31
  initialSearchInput?: string;
32
32
  sessionId?: string;
33
+ isFastForProvider?: (provider?: string) => boolean;
34
+ isFastForSubagentProvider?: (provider?: string) => boolean;
33
35
  });
34
36
  handleInput(keyData: string): void;
35
37
  getSearchInput(): Input;
@@ -10,5 +10,6 @@ export declare class OAuthSelectorComponent extends Container {
10
10
  requestRender?: () => void;
11
11
  });
12
12
  stopValidation(): void;
13
+ dispose(): void;
13
14
  handleInput(keyData: string): void;
14
15
  }
@@ -20,6 +20,7 @@ export interface MCPAddWizardOAuthResult {
20
20
  export declare class MCPAddWizard extends Container {
21
21
  #private;
22
22
  constructor(onComplete: (name: string, config: MCPServerConfig, scope: Scope) => void, onCancel: () => void, onOAuth?: (authUrl: string, tokenUrl: string, clientId: string, clientSecret: string, scopes: string) => Promise<MCPAddWizardOAuthResult>, onTestConnection?: (config: MCPServerConfig) => Promise<void>, onRender?: () => void, initialName?: string);
23
+ dispose(): void;
23
24
  handleInput(keyData: string): void;
24
25
  }
25
26
  export {};
@@ -47,6 +47,7 @@ export declare class ToolExecutionComponent extends Container {
47
47
  * Stop spinner animation and cleanup resources.
48
48
  */
49
49
  stopAnimation(): void;
50
+ dispose(): void;
50
51
  setExpanded(expanded: boolean): void;
51
52
  setShowImages(show: boolean): void;
52
53
  invalidate(): void;
@@ -93,6 +93,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
93
93
  locallySubmittedUserSignatures: Set<string>;
94
94
  lastSigintTime: number;
95
95
  lastEscapeTime: number;
96
+ lastComposerClearEscapeTime: number;
96
97
  shutdownRequested: boolean;
97
98
  hookSelector: HookSelectorComponent | undefined;
98
99
  hookInput: HookInputComponent | undefined;
@@ -100,6 +100,7 @@ export interface InteractiveModeContext {
100
100
  locallySubmittedUserSignatures: Set<string>;
101
101
  lastSigintTime: number;
102
102
  lastEscapeTime: number;
103
+ lastComposerClearEscapeTime: number;
103
104
  shutdownRequested: boolean;
104
105
  hookSelector: HookSelectorComponent | undefined;
105
106
  hookInput: HookInputComponent | undefined;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared runtime lifecycle foundation.
3
+ *
4
+ * Two minimal, deliberately small primitives that subsystem runtimes
5
+ * (DAP/LSP/MCP stdio, eval workers, etc.) adopt so spawned children and
6
+ * non-process resources cannot outlive their owner:
7
+ *
8
+ * F1(a) `spawnOwnedProcess` — wraps `ptree.spawn` with explicit
9
+ * process-group ownership, escalating (SIGTERM -> grace -> SIGKILL)
10
+ * tree termination, bounded `awaitExit`, abort-listener cleanup on
11
+ * settle, idempotent `dispose`, and a single postmortem hook that
12
+ * reaps every still-live owned process group on fatal/normal shutdown.
13
+ *
14
+ * F1(b) `registerResourceOwner` — a generic, idempotent postmortem adapter
15
+ * for non-process resources (Bun Workers, VM contexts, timers,
16
+ * sockets) built on the existing `postmortem.register` facility.
17
+ *
18
+ * Ownership is keyed to the *process group*, not the root process. A root that
19
+ * exits after backgrounding descendants (`sh -c "worker & exit 0"`) keeps the
20
+ * owner registered until the group is actually gone, so the descendant tree is
21
+ * still reaped by `dispose()`/postmortem.
22
+ *
23
+ * This module intentionally owns only these primitives. It does not migrate
24
+ * existing call sites; subsystem PRs adopt it incrementally.
25
+ *
26
+ * Note: `ptree.spawn` always pipes stdout/stderr. Adopters that expect output
27
+ * (DAP/LSP/MCP protocol servers) must consume `owner.child.stdout`; F1 does not
28
+ * drain it, so a chatty child whose stdout is never read can still block on a
29
+ * full pipe. That draining is the adopter's responsibility.
30
+ */
31
+ import { ptree } from "@gajae-code/utils";
32
+ /** Options for {@link spawnOwnedProcess}. */
33
+ export interface SpawnOwnedOptions {
34
+ cwd?: string;
35
+ env?: Record<string, string | undefined>;
36
+ /** stdin mode passed through to the child. Defaults to `"ignore"`. */
37
+ stdin?: "pipe" | "ignore";
38
+ /** When aborted, the owned process tree is disposed (escalating kill). */
39
+ signal?: AbortSignal;
40
+ /** Grace period (ms) between SIGTERM and SIGKILL on dispose. Default 2000. */
41
+ gracefulMs?: number;
42
+ /**
43
+ * Spawn the child as its own process-group leader so the whole descendant
44
+ * tree can be signalled on dispose. Defaults to `true` on POSIX. Has no
45
+ * effect on Windows, where teardown falls back to single-process kill.
46
+ */
47
+ processGroup?: boolean;
48
+ /** Label used in diagnostics. */
49
+ name?: string;
50
+ }
51
+ /** Result of a bounded {@link OwnedProcess.awaitExit}. */
52
+ export interface AwaitExitResult {
53
+ /** `true` when the process has exited; `false` when the timeout fired first. */
54
+ exited: boolean;
55
+ /** Exit code if known, else `null`. */
56
+ code: number | null;
57
+ }
58
+ /** A spawned child process owned by the runtime with guaranteed teardown. */
59
+ export interface OwnedProcess {
60
+ readonly child: ptree.ChildProcess;
61
+ readonly pid: number | undefined;
62
+ /** Resolves/rejects when the root child exits (mirrors ptree's `exited`). */
63
+ readonly exited: Promise<number>;
64
+ /** `true` once `dispose()` has started. */
65
+ readonly disposed: boolean;
66
+ /**
67
+ * Wait for the root child to exit, optionally bounded by `timeoutMs`. With no
68
+ * timeout it resolves only when the child exits. Never rejects.
69
+ */
70
+ awaitExit(opts?: {
71
+ timeoutMs?: number;
72
+ }): Promise<AwaitExitResult>;
73
+ /**
74
+ * Idempotently terminate the owned process *group*: SIGTERM the group, wait
75
+ * `gracefulMs`, then SIGKILL, polling group liveness throughout. Removes the
76
+ * abort listener and deregisters from the live-owner set only after teardown
77
+ * has completed. Repeated/concurrent calls return the same in-flight promise.
78
+ */
79
+ dispose(): Promise<void>;
80
+ }
81
+ /**
82
+ * Spawn a child process owned by the runtime. The returned {@link OwnedProcess}
83
+ * is registered for postmortem cleanup and tears down its whole process group
84
+ * on dispose/abort.
85
+ */
86
+ export declare function spawnOwnedProcess(cmd: string[], opts?: SpawnOwnedOptions): OwnedProcess;
87
+ /** Number of currently live owned processes. Exposed for leak assertions/tests. */
88
+ export declare function liveOwnedProcessCount(): number;
89
+ /** Dispose every live owned process. For owner-scoped teardown and tests. */
90
+ export declare function disposeAllOwnedProcesses(): Promise<void>;
91
+ type ResourceDisposer = () => void | Promise<void>;
92
+ /**
93
+ * Register a non-process resource for postmortem/fatal-exit cleanup.
94
+ *
95
+ * Idempotent by `name`: re-registering the same name replaces the prior
96
+ * disposer (last wins). Returns an unregister function that removes the owner
97
+ * only while it is still the active registration for that name.
98
+ */
99
+ export declare function registerResourceOwner(name: string, disposer: ResourceDisposer): () => void;
100
+ /** Number of registered resource owners. Exposed for leak assertions/tests. */
101
+ export declare function resourceOwnerCount(): number;
102
+ /**
103
+ * Run and clear every registered resource disposer. Attempts all disposers even
104
+ * if some throw, then surfaces the failures as an `AggregateError` so callers
105
+ * can distinguish "all closed" from "a resource may still be alive".
106
+ */
107
+ export declare function disposeAllResourceOwners(): Promise<void>;
108
+ export {};
@@ -14,6 +14,7 @@ export declare class StdioTransport implements MCPTransport {
14
14
  onRequest?: (method: string, params: unknown) => Promise<unknown>;
15
15
  constructor(config: MCPStdioServerConfig);
16
16
  get connected(): boolean;
17
+ get closeBeforeReconnect(): true;
17
18
  /**
18
19
  * Start the subprocess and begin reading.
19
20
  */
@@ -185,6 +185,8 @@ export interface MCPTransport {
185
185
  notify(method: string, params?: Record<string, unknown>): Promise<void>;
186
186
  /** Close the transport */
187
187
  close(): Promise<void>;
188
+ /** Whether close must finish before reconnect can safely spawn a replacement. */
189
+ readonly closeBeforeReconnect?: boolean;
188
190
  /** Whether the transport is connected */
189
191
  readonly connected: boolean;
190
192
  /** Event handlers */
@@ -13,7 +13,7 @@
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
15
  import { type Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type StablePrefixSnapshot, ThinkingLevel } from "@gajae-code/agent-core";
16
- import { type CompactionResult } from "@gajae-code/agent-core/compaction";
16
+ import { type CompactionResult, type EmergencyCompactionSample } from "@gajae-code/agent-core/compaction";
17
17
  import type { AssistantMessage, Effort, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@gajae-code/ai";
18
18
  export interface ForkContextSeedMetadata {
19
19
  sourceSessionId: string;
@@ -407,6 +407,18 @@ export declare class AgentSession {
407
407
  * Call this when completely done with the session.
408
408
  */
409
409
  dispose(): Promise<void>;
410
+ /**
411
+ * Bounded, best-effort teardown of the subprocess-spawning resources this session
412
+ * owns: the browser tool's headless/spawned Chrome and the Python eval kernel + JS VM
413
+ * contexts. Unlike {@link dispose}, this touches only child processes and is time-boxed,
414
+ * so a top-level `SIGINT`/`SIGTERM`/`SIGHUP` handler can run it without hanging — without
415
+ * it, an external kill bypasses `dispose()` and orphans Chrome/Python to PID 1 (#698).
416
+ *
417
+ * Idempotent: every step is a no-op once the graceful {@link dispose} path has released
418
+ * the resources. Never throws; per-step failures are logged and the whole run is capped
419
+ * at `timeoutMs` so a wedged subprocess can't stall process exit.
420
+ */
421
+ disposeChildSubprocesses(timeoutMs?: number): Promise<void>;
410
422
  /** Full agent state */
411
423
  get state(): AgentState;
412
424
  /** Current model (may be undefined if not yet selected) */
@@ -726,6 +738,20 @@ export declare class AgentSession {
726
738
  * {@link isFastModeActive} instead — that one respects the model's provider.
727
739
  */
728
740
  isFastModeEnabled(): boolean;
741
+ /**
742
+ * True when the configured `serviceTier` resolves to `"priority"` for the
743
+ * given model `provider`. Returns false for scoped tiers that don't match
744
+ * (e.g. `"openai-only"` on an anthropic provider) and when `provider` is
745
+ * undefined. This is the canonical provider-aware fast-mode predicate.
746
+ */
747
+ isFastForProvider(provider?: string): boolean;
748
+ /**
749
+ * Provider-aware fast-mode predicate for task-tool subagent roles, evaluated
750
+ * against the effective subagent tier (`task.serviceTier`) rather than the
751
+ * main session tier. Use this for `task.agentModelOverrides` role rows so the
752
+ * ⚡ glyph reflects the tier the subagent actually runs under.
753
+ */
754
+ isFastForSubagentProvider(provider?: string): boolean;
729
755
  /**
730
756
  * True when the configured `serviceTier` resolves to `"priority"` for the
731
757
  * *currently selected model's provider*. Returns false for scoped tiers
@@ -789,6 +815,8 @@ export declare class AgentSession {
789
815
  */
790
816
  handoff(customInstructions?: string, options?: SessionHandoffOptions): Promise<HandoffResult | undefined>;
791
817
  prepareContributionPrep(options?: ContributionPrepOptions): Promise<ContributionPrepResult>;
818
+ /** Test seam: override the emergency-compaction resource sampler so tests never read real RSS. */
819
+ setResourceSampler(sampler: () => EmergencyCompactionSample): void;
792
820
  /**
793
821
  * Toggle auto-compaction setting.
794
822
  */
@@ -1,3 +1,6 @@
1
+ export interface ArtifactSaveOptions {
2
+ maxBytes?: number;
3
+ }
1
4
  /**
2
5
  * Manages artifact storage for a session.
3
6
  *
@@ -40,7 +43,7 @@ export declare class ArtifactManager {
40
43
  * @param toolType Tool name for file extension (e.g., "bash", "read")
41
44
  * @returns Artifact ID (numeric string)
42
45
  */
43
- save(content: string, toolType: string): Promise<string>;
46
+ save(content: string, toolType: string, options?: ArtifactSaveOptions): Promise<string>;
44
47
  /**
45
48
  * Check if an artifact exists.
46
49
  * @param id Artifact ID (numeric string)
@@ -2,6 +2,7 @@ import type { AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
2
  export declare const DEFAULT_MAX_LINES = 3000;
3
3
  export declare const DEFAULT_MAX_BYTES: number;
4
4
  export declare const DEFAULT_MAX_COLUMN = 1024;
5
+ export declare const DEFAULT_ARTIFACT_MAX_BYTES: number;
5
6
  export interface OutputSummary {
6
7
  output: string;
7
8
  truncated: boolean;
@@ -19,6 +20,8 @@ export interface OutputSummary {
19
20
  columnTruncatedLines?: number;
20
21
  /** Artifact ID for internal URL access (artifact://<id>) when truncated */
21
22
  artifactId?: string;
23
+ /** Bytes omitted from artifact storage after the artifact hard cap was reached. */
24
+ artifactTruncatedBytes?: number;
22
25
  }
23
26
  export interface OutputSinkOptions {
24
27
  artifactPath?: string;
@@ -38,6 +41,8 @@ export interface OutputSinkOptions {
38
41
  * writes still respect the budget. Default 0 = no per-line cap.
39
42
  */
40
43
  maxColumns?: number;
44
+ /** Hard cap for artifact writes/pending replay. Default DEFAULT_ARTIFACT_MAX_BYTES. */
45
+ artifactMaxBytes?: number;
41
46
  onChunk?: (chunk: string) => void;
42
47
  /** Minimum ms between onChunk calls. 0 = every chunk (default). */
43
48
  chunkThrottleMs?: number;
@@ -52,6 +57,13 @@ export interface OutputSinkOptions {
52
57
  * relative to the sink (the sink does not catch errors from this callback).
53
58
  */
54
59
  onRawChunk?: (chunk: string) => void;
60
+ /**
61
+ * Opt-in (F21): when true, sanitization + live callback delivery + retention are coalesced over
62
+ * batched raw chunks instead of run per chunk, bounding sync CPU for many-small-chunk output. The
63
+ * raw artifact mirror stays byte-correct. Defaults to the PI_OUTPUT_SANITIZE_COALESCE env flag
64
+ * (default OFF — the per-chunk path is byte-identical to historical behavior).
65
+ */
66
+ coalesceSanitize?: boolean;
55
67
  }
56
68
  export interface TruncationResult {
57
69
  content: string;
@@ -0,0 +1,76 @@
1
+ import type { Model } from "@gajae-code/ai";
2
+ /**
3
+ * A single line in the `/fast status` report: a labelled model and whether fast
4
+ * mode is effective for it. The `fast` flag is resolved by the caller
5
+ * (`buildFastStatusReport`) so each row can use the correct service tier — the
6
+ * main session tier for the current model / `modelRoles` roles, or the subagent
7
+ * tier (`task.serviceTier`) for `task.agentModelOverrides` roles.
8
+ */
9
+ export interface FastStatusRow {
10
+ /** Display label, e.g. "현재 모델", "DEFAULT", "EXECUTOR". */
11
+ label: string;
12
+ /** Resolved model for this row, if any. */
13
+ model?: Model;
14
+ /** Whether fast mode is effective for this row's model. */
15
+ fast: boolean;
16
+ }
17
+ export interface FormatFastStatusReportArgs {
18
+ rows: FastStatusRow[];
19
+ /** The active theme's fast icon token (`theme.icon.fast`). */
20
+ iconFast: string;
21
+ /** Optional decorator for inactive ("off") text, e.g. theme dim in the TUI. */
22
+ formatInactive?: (text: string) => string;
23
+ }
24
+ /** Title line of the `/fast status` report. */
25
+ export declare const FAST_STATUS_TITLE = "Fast \uBAA8\uB4DC \uC0C1\uD0DC";
26
+ /** The inactive marker shown for rows where fast mode does not apply. */
27
+ export declare const FAST_STATUS_OFF = "off";
28
+ /**
29
+ * Format a multiline `/fast status` report. Pure and shared by the CLI
30
+ * (`handle`) and TUI (`handleTui`) command branches so the two never drift.
31
+ * Each row's fast/off state is decided by the caller (see
32
+ * {@link buildFastStatusReport}) so per-row service-tier differences are honored.
33
+ */
34
+ export declare function formatFastStatusReport(args: FormatFastStatusReportArgs): string;
35
+ /** Minimal session surface needed to build the `/fast status` report. */
36
+ export interface FastStatusSessionLike {
37
+ readonly model?: Model;
38
+ /** Fast predicate against the main session tier (current model + `modelRoles`). */
39
+ isFastForProvider(provider?: string): boolean;
40
+ /** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
41
+ isFastForSubagentProvider(provider?: string): boolean;
42
+ resolveRoleModelWithThinking(role: string): {
43
+ model?: Model;
44
+ };
45
+ }
46
+ /** A role to enumerate in the report, with the tier source its subagent runs under. */
47
+ export interface FastStatusRoleTarget {
48
+ id: string;
49
+ label: string;
50
+ /**
51
+ * True for `task.agentModelOverrides` roles (executor/architect/planner/critic)
52
+ * that run under `task.serviceTier`; false for `modelRoles` roles (default)
53
+ * that run under the main session tier.
54
+ */
55
+ isSubagentRole: boolean;
56
+ }
57
+ export interface BuildFastStatusReportArgs {
58
+ session: FastStatusSessionLike;
59
+ /** Role targets to enumerate, in display order. */
60
+ roleTargets: ReadonlyArray<FastStatusRoleTarget>;
61
+ /** The active theme's fast icon token (`theme.icon.fast`). */
62
+ iconFast: string;
63
+ /** Optional decorator for inactive ("off") text, e.g. theme dim in the TUI. */
64
+ formatInactive?: (text: string) => string;
65
+ }
66
+ /**
67
+ * Build the `/fast status` report from a live session: the active/current model
68
+ * followed by each assigned role (subagent) model. Unassigned roles are skipped
69
+ * so the report mirrors the `/model` selector, which only badges assigned roles.
70
+ *
71
+ * Subagent roles (`task.agentModelOverrides`) are evaluated against the
72
+ * effective subagent tier (`task.serviceTier`), while the current model and
73
+ * `modelRoles` roles use the main session tier — matching where each model
74
+ * actually runs.
75
+ */
76
+ export declare function buildFastStatusReport(args: BuildFastStatusReportArgs): string;
@@ -6,6 +6,7 @@ import { type Theme } from "../modes/theme/theme";
6
6
  import type { ToolSession } from ".";
7
7
  import { type OutputMeta } from "./output-meta";
8
8
  export declare const BASH_DEFAULT_PREVIEW_LINES = 10;
9
+ export declare function saveBashOriginalArtifactForTests(session: ToolSession, originalText: string): Promise<string | undefined>;
9
10
  declare const bashSchemaBase: z.ZodObject<{
10
11
  command: z.ZodString;
11
12
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -26,6 +26,8 @@ export interface TabSession {
26
26
  pending: Map<string, PendingRun>;
27
27
  dialogPolicy?: DialogPolicy;
28
28
  kindTag: BrowserKindTag;
29
+ /** Session that acquired this tab; used for session-scoped teardown (F13). */
30
+ ownerId?: string;
29
31
  }
30
32
  export interface AcquireTabOptions {
31
33
  url?: string;
@@ -39,6 +41,8 @@ export interface AcquireTabOptions {
39
41
  signal?: AbortSignal;
40
42
  timeoutMs: number;
41
43
  dialogs?: DialogPolicy;
44
+ /** Owning session id so dispose can release only this session's tabs (F13). */
45
+ ownerId?: string;
42
46
  }
43
47
  export interface AcquireTabResult {
44
48
  tab: TabSession;
@@ -58,6 +62,11 @@ export declare function acquireTab(name: string, browser: BrowserHandle, opts: A
58
62
  export declare function runInTab(name: string, opts: RunInTabOptions): Promise<RunResultOk>;
59
63
  export declare function releaseTab(name: string, opts?: ReleaseTabOptions): Promise<boolean>;
60
64
  export declare function releaseAllTabs(opts?: ReleaseTabOptions): Promise<number>;
65
+ /**
66
+ * Release only the tabs owned by `ownerId` (F13 session-scoped teardown). Tabs acquired
67
+ * by other sessions (or with no owner) are left untouched. No-op for a null/empty owner.
68
+ */
69
+ export declare function releaseTabsForOwner(ownerId: string | null | undefined, opts?: ReleaseTabOptions): Promise<number>;
61
70
  export declare function dropHeadlessTabs(): Promise<void>;
62
71
  export declare function initializeTabWorkerForTest(worker: WorkerHandle, payload: WorkerInitPayload, timeoutMs: number): Promise<ReadyInfo>;
63
72
  export {};
@@ -60,9 +60,10 @@ export declare function getRowByKey(db: Database, table: string, pk: {
60
60
  type?: string;
61
61
  }, key: string): Record<string, unknown> | null;
62
62
  export declare function getRowByRowId(db: Database, table: string, key: string): Record<string, unknown> | null;
63
- export declare function executeReadQuery(db: Database, sql: string): {
63
+ export declare function executeReadQuery(db: Database, sql: string, maxRows?: number): {
64
64
  columns: string[];
65
65
  rows: Record<string, unknown>[];
66
+ truncated: boolean;
66
67
  };
67
68
  export declare function insertRow(db: Database, table: string, data: Record<string, unknown>): void;
68
69
  export declare function updateRowByKey(db: Database, table: string, pk: {
@@ -14,12 +14,12 @@ export interface CodexSearchParams {
14
14
  * Executes a web search using OpenAI code provider's built-in web search tool.
15
15
  *
16
16
  * Default-model behavior:
17
- * - If `PI_OPENAI_CODE_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
17
+ * - If `PI_CODEX_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
18
18
  * upstream error verbatim.
19
- * - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.4, GPT-5
20
- * OpenAI code backend, GPT-5, …) and retry the next candidate only when OpenAI code backend returns the
19
+ * - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.5, GPT-5.4,
20
+ * GPT-5 code backend, …) and retry the next candidate only when OpenAI code backend returns the
21
21
  * known 400 "model is not supported" family. This avoids selecting
22
- * `gpt-5-OpenAI code backend-mini` first on ChatGPT accounts, which OpenAI rejects.
22
+ * `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI rejects.
23
23
  */
24
24
  export declare function searchCodex(params: SearchParams): Promise<SearchResponse>;
25
25
  /**