@gajae-code/coding-agent 0.17.1 → 0.17.2

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 (53) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/types/async/job-manager.d.ts +7 -6
  3. package/dist/types/config/autorouting-tier-map.d.ts +6 -0
  4. package/dist/types/config/file-lock.d.ts +1 -0
  5. package/dist/types/config/model-profile-activation.d.ts +18 -3
  6. package/dist/types/config/model-registry.d.ts +9 -0
  7. package/dist/types/coordinator-mcp/server.d.ts +3 -0
  8. package/dist/types/modes/acp/acp-agent.d.ts +40 -1
  9. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  10. package/dist/types/sdk/acp/adapter.d.ts +27 -0
  11. package/dist/types/sdk/acp/final-text.d.ts +8 -0
  12. package/dist/types/sdk/broker/ensure.d.ts +46 -1
  13. package/dist/types/sdk/prompt-deadline-manager.d.ts +1 -1
  14. package/dist/types/sdk/prompt-failure.d.ts +30 -1
  15. package/dist/types/sdk/prompt-status.d.ts +8 -0
  16. package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
  17. package/dist/types/sdk/transport/serve-cli.d.ts +27 -0
  18. package/dist/types/thinking.d.ts +3 -1
  19. package/dist/types/utils/mupdf-wasm-embedded.d.ts +2 -0
  20. package/package.json +7 -7
  21. package/src/async/job-manager.ts +38 -8
  22. package/src/commands/sdk.ts +78 -4
  23. package/src/config/autorouting-tier-map.ts +2 -0
  24. package/src/config/file-lock.ts +52 -17
  25. package/src/config/model-profile-activation.ts +72 -10
  26. package/src/config/model-registry.ts +21 -0
  27. package/src/coordinator-mcp/server.ts +21 -6
  28. package/src/debug/report-bundle.ts +114 -24
  29. package/src/gjc-runtime/managed-owner-admission.ts +67 -50
  30. package/src/internal-urls/docs-index.generated.ts +7 -6
  31. package/src/modes/acp/acp-agent.ts +187 -14
  32. package/src/modes/acp/acp-event-mapper.ts +9 -0
  33. package/src/modes/components/model-selector.ts +31 -15
  34. package/src/modes/components/status-line/segments.ts +6 -1
  35. package/src/modes/components/thinking-selector.ts +7 -3
  36. package/src/modes/controllers/selector-controller.ts +16 -3
  37. package/src/sdk/acp/adapter.ts +63 -1
  38. package/src/sdk/acp/final-text.ts +19 -2
  39. package/src/sdk/broker/ensure.ts +97 -19
  40. package/src/sdk/bus/index.ts +31 -4
  41. package/src/sdk/host/session-runtime.test.ts +498 -7
  42. package/src/sdk/host/session-runtime.ts +188 -21
  43. package/src/sdk/prompt-deadline-manager.ts +3 -3
  44. package/src/sdk/prompt-failure.ts +43 -1
  45. package/src/sdk/prompt-status.ts +8 -0
  46. package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
  47. package/src/sdk/router/session-router.ts +22 -3
  48. package/src/sdk/session.ts +12 -9
  49. package/src/sdk/transport/serve-cli.ts +105 -15
  50. package/src/session-import/codex.ts +12 -3
  51. package/src/thinking.ts +21 -2
  52. package/src/utils/mupdf-wasm-embedded.ts +3 -0
  53. package/src/utils/mupdf-wasm.ts +24 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,78 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.17.2] - 2026-09-18
6
+
7
+ ### Added
8
+
9
+ - Add a project-local Bash pre-tool hook example that invokes HOL Guard directly and blocks execution unless Guard classifies the command as explicitly benign with an `allow` minimum action.
10
+
11
+ ### Fixed
12
+
13
+ - The detached-owner lifecycle suite (`gjc harness start --detach`, B1) no longer skips wholesale on non-Linux hosts. Its `tmux` is a bash fixture and its cgroup inputs are injected through `GJC_HARNESS_TEST_*`, so six of its seven cases carried no real Linux dependency yet only ever ran in CI — leaving a red `dev` undiagnosable for anyone developing on macOS. Only the case that drives the Linux-only `unsafe_service` cgroup branch stays gated.
14
+ - An owner-routed `harness finalize` that never reaches its owner answers `{ completed: false, reason: "owner-not-live" }` with no `finalize` key, so the suite's assertion reported a bare `Received: undefined` and hid the routing reason. It now projects that reason into the assertion and names it.
15
+
16
+ - The coordinator platform-capability test no longer swaps `process.platform` process-wide (which made valid native lock publication receipts look invalid before the artifact handler ran); platform discovery and refusal are now driven entirely through the resolved server platform option.
17
+
18
+ - Endpoint-manager ownership is now released only after disposal and retained async authority actually settle, so a replacement endpoint can no longer be admitted while the outgoing one is still tearing down. Disposal is idempotent, rekey applies the same guard, and SDK startup rollback no longer eagerly unregisters an endpoint that has not finished disposing (#5603).
19
+
20
+ - The detached SDK broker's discovery budget is now published as a composition (`BROKER_DISCOVERY_BUDGET`) instead of three module-private constants and a restated sum, and every deadline and poll on that path runs on an injectable clock. The spawn single-flight regression now spends that budget one leg at a time on a virtual clock, so each leg's cost is measured exactly and a change to any single constant fails an assertion in milliseconds instead of silently re-tuning a wall-clock race (#5604).
21
+
22
+ - Added real external stdio ACP regression coverage and documented stale-broker startup-control provenance failures.
23
+
24
+ - A prompt failure that reaches an ACP client now carries the classification the terminal already computed, instead of collapsing to an untyped `-32603` with `{code: "prompt_failed", details: "Prompt submission failed."}` (issue #5615). The failure's execution `phase`, its bounded origin `category`, and the provider classifier that produced it were derived at settlement and carried on the rejection, then discarded at the single JSON-RPC translation point, so a client watching a mid-session turn die could not tell a transient provider blip from a session that will never answer again without parsing an English message. The JSON-RPC code stays `-32603` (pinned ACP core-v1 conformance expects it for this class) and the redacted message is unchanged; the classification travels in `data`, which additionally exposes a derived `retryability` of `transient`, `terminal`, or `unknown` so a caller does not have to re-implement the category matrix. Only `provider_transport` is transient — the same class the shipped first-turn gate already treats as re-submittable — while a provider rejection, a deadline, and an agent runtime failure are terminal for that turn, and an uncertain attribution stays `unknown` rather than being guessed. `providerCode` is forwarded only when it is a bounded safe token and omitted otherwise, so the redaction contract still holds on this newly exposed field. Retry behaviour is unchanged.
25
+
26
+ - A first-turn startup-readiness failure whose terminal carries only whitespace as final text is now retried once instead of being surfaced as a hard failure, because whitespace carries no assistant content, matching the trimmed presence contract already applied by prompt reconciliation.
27
+ - Such a terminal no longer publishes its whitespace as an assistant message chunk alongside the retry's real answer: final-text resolution and the terminal publication gate now share one trimmed presence predicate, which also suppresses a whitespace-only final-text delta.
28
+
29
+ - Chat daemon replay retention-gap concessions now warn once per session stream and gap range. Reconnect and polling duplicates are demoted to debug, while a new gap range still emits the original warning; the bounded memo prevents long-running daemons from retaining unbounded stream history (issue #5619).
30
+
31
+ - Restore MiniMax-M3 reasoning on/off controls in model selection and reasoning menus for the international and China MiniMax and Coding Plan routes. Send the native `adaptive`/`disabled` switch instead of unsupported reasoning-effort or token-budget parameters, and explicitly disable thinking when the agent selects off (#5626).
32
+ - Show MiniMax M2.x reasoning as always on rather than offering ineffective effort or off controls. Keep model-role labels and the status line consistent with these capabilities.
33
+ - Restrict MiniMax-native thinking behavior to validated first-party HTTPS endpoints and API paths, including the resolved endpoint at request time. Custom proxies retaining a MiniMax provider ID no longer inherit the native thinking contract.
34
+
35
+ - `gjc sdk serve` no longer surfaces a session-selection failure as an uncaught exception with the failure code buried in a message string. Selection failures (`not_found`, `ambiguous_session`, `endpoint_stale`, `no_live_endpoint`, `multiple_live_endpoints`) and the broker-reach failures on the same path now raise a typed error carrying a stable `code`, a broker `SdkClientError` keeps both its `code` and its `details` instead of being downgraded, and the command boundary writes a `{"ok":false,"error":{"code","message"}}` envelope to stderr — never the stdout frame channel — and exits 1 (#5633).
36
+ - `gjc sdk serve` no longer lets broker teardown replace the failure it is reporting. A rejected `broker.close()` in the cleanup path used to overwrite the typed serve error with its own; the primary failure now always wins and the teardown failure rides alongside it as `cleanupError`. When teardown is the only thing that failed, it surfaces as a typed `serve_cleanup_failed` instead of an untyped rejection, and teardown still always runs (#5633).
37
+ - `gjc sdk serve` no longer escapes untyped when the broker discovery record itself is unreadable. A permission, file-kind, or unsupported-state-version failure reading `<agent dir>/sdk/broker.json` used to throw past the command boundary with no stderr envelope and no exit code; it now raises the typed `broker_discovery_unreadable` with the record's path in `details`, never its token. A missing record still reports `broker_unavailable` (#5633).
38
+ - `gjc sdk serve` no longer starts a relay on an endpoint record with an empty credential. A missing, non-string, or empty `token` used to become `""` and be written into the upstream WebSocket URL, where it resurfaced much later as a generic transport failure; it is now refused at the broker boundary with the same typed `unavailable` failure an invalid `url` already produces, before either the stdio or the socket relay starts (#5633).
39
+ - The `gjc sdk serve` stderr envelope no longer drops the broker-teardown diagnostics it records. When a serve failure and a failing `broker.close()` coincide, the envelope now carries `error.cleanupError` alongside `code`, `message` and `details`, so an embedder can see that the session may not have been released cleanly. An envelope with no teardown failure is unchanged — the key is absent, not present and empty (#5633).
40
+
41
+ - Codex session import now recognizes the same credential shapes as its sibling import scrubber. `session-import/codex.ts` listed only `ghp_` of the five GitHub token prefixes and only `AKIA` of the four AWS access-key id prefixes, and knew none of the Google, Slack, npm, GitLab, or Hugging Face shapes that `session-import/redact.ts` already covers — reaching a new session transcript verbatim from an imported Codex file. Stripe restricted keys (`rk_live_`/`rk_test_`) are included: the generic `sk` branch reached the secret-key forms only because they share that prefix. The cheap pre-check that gates the scan is widened with it so the added shapes are actually reached.
42
+
43
+ - Documentation now states the real `sdk.promptDeadlineMs` default. Raising it from 30 to 60 minutes updated the schema but left both READMEs, the Japanese and Korean translations, `docs/sdk.md`, `docs/bot-integration.md`, `docs/hermes-mcp-bridge.md`, `docs/acp-local-development.md`, and the shipped `gjc-sdk-operate` skill telling SDK clients their turn dies after 30 minutes. A new parity test derives the expected figure from the schema default rather than restating it, so the next change to the default fails the build instead of silently misinforming callers (#5637).
44
+
45
+ - The terminal-abort-chain test suite no longer leaks temp agent-state directories. The per-case `finally` already reclaimed the directory on the throwing path, but two paths escaped it: an `afterEach` that exceeds its hook budget is abandoned mid-await so the `finally` never runs, and a writer that outlives teardown can recreate a root the `finally` already removed. Each root is now registered on creation and swept from `afterAll`, which runs in both cases and re-sweeps after a short settle window to catch late writers. A start-of-suite reap also removes roots left by earlier runs, but only ones it can prove are abandoned: each root carries a marker naming the owning pid and host, and a root is removed only when the marker is well-formed, names this host, names a process other than the running one, that process probes as exited (`ESRCH`), and the two-hour age gate has passed. A probe the OS refuses to answer, a missing or malformed marker, and a foreign host all keep the root, so a live shard's directory is never deleted; roots written before the marker existed are consequently never reaped. Measured on macOS, where `os.tmpdir()` is `$TMPDIR` rather than `/tmp`: nine leaked roots per run before, zero after (#5665).
46
+
47
+ - An ACP prompt abandoned by the inactivity watchdog now reports whether the turn's plan was still unfinished instead of collapsing into a generic internal error. The rejection keeps its `prompt_abandoned` code and message, and its JSON-RPC `data` additionally carries `planIncomplete`, `planPendingCount`, `planTotalCount`, and a bounded `planPending` list of the steps the turn never completed, so a wrapper can tell "the agent finished" from "the agent stopped with work left" instead of publishing unverified work as complete. An abandon that never observed a plan omits those fields entirely rather than guessing. A step dropped via `/todo drop` (internal status `abandoned`) counts as unfinished too, even though ACP's three-value plan status projects it as `completed` for the client's plan UI (#5669).
48
+
49
+ - Compiled binaries no longer abort at startup with `SyntaxError: Unexpected identifier 'init_model_registry'`. Resolving MuPDF's embedded WASM no longer makes its module asynchronous, so the bundler keeps emitting a valid module initializer for the `model-registry`/`model-resolver` import cycle.
50
+
51
+ - Managed owner admission now says why it blocked a child. A child that fails closed writes the
52
+ platform and the exact-evidence reader state into its durable handoff and prints one reason line
53
+ to stderr before exiting 75, instead of exiting silently. On platforms without the Linux-only
54
+ recovery-fs authority this reports `exact binding reader unsupported` rather than leaving the
55
+ operator with only the broker's registration-uncertain timeout.
56
+
57
+ - File-lock acquisition exhaustion now reports the real liveness of a holder running on this host instead of calling every host-qualified record "liveness unknown from this host". A lock owned under this installation's current or previous host identity is probed and described as live, dead but not reaped, or indeterminate; only a genuinely foreign owner stays opaque. Reclamation authority is unchanged — the diagnostic and the stale verdict now share one host predicate so they can never disagree about whose PID a record names (#5653).
58
+ - A file-lock holder whose record carries no host identity at all is now described as held "on an unrecorded host" instead of "on host undefined". Such a record is still treated as foreign by a host-aware acquirer — its PID is never probed and its lock is never reclaimed — so only the wording changes (#5653).
59
+
60
+ - ACP prompts no longer hang forever in provider preflight. The inactivity watchdog is now armed from the moment the prompt owns the session, so a `session/prompt` blocked in `ensureProviders()` is rejected as `prompt_abandoned` — naming provider preflight as the phase — instead of reporting `running` with no output, no tool calls and no error.
61
+ - SDK provider activation no longer retries without limit. The registration loop now has an attempt cap and a wall-clock budget, and exhaustion throws `provider_activation_exhausted` naming the contended capability, so lease contention between concurrent session hosts fails attributably instead of spinning.
62
+
63
+ - Model profile activation now diagnoses a required provider this build does not know (neither a built-in provider id nor declared in `models.yml`) as a build/config mismatch with the exact provider names, instead of telling the user to run `/login` for a provider the running binary cannot serve — for example a profile authored on a newer or custom build.
64
+
65
+ - npm-installed source commands no longer resolve MuPDF's embedded WASM through a monorepo-only path. Source installs let MuPDF load its adjacent sidecar, while compiled binaries keep the embedded asset used for PDF conversion.
66
+
67
+ - Allow explicitly configured preset proxy routing through automatically discovered OpenCodex in activation, the preset picker, and SDK availability queries, matching upstream wire model IDs while preserving stored GJC selectors and direct-provider fallback preferences.
68
+
69
+ - A prompt that reaches its SDK deadline now delivers the correlated terminal failure frame to ACP and other SDK clients instead of leaving `session/prompt` waiting behind the watchdog (issue #5583).
70
+ - A prompt whose deadline expires while its real `agent_end` is in flight now publishes exactly one correlated terminal boundary instead of two, so SDK clients no longer see a duplicate `agent_end` for a turn that reconciled to a single durable outcome.
71
+ - A busy session no longer republishes a terminal boundary it already delivered. The single-boundary claim is bounded by capacity, and a publisher parked on its durable writes could have its claim evicted by later turns, so its delayed `agent_end` published a second boundary for an already-terminalized correlation. A correlation with a publisher still able to reach its claim is now never evicted.
72
+ - A prompt deadline that fails to publish its correlated diagnostic still delivers its correlated terminal boundary. The two frames were emitted as one unguarded sequence, so a failure on the first suppressed the second and left the request with no terminal frame at all; each frame is now published independently and a publication failure is logged instead of swallowed.
73
+ - A prompt that reaches its deadline on the notification-bus route — the route a live `--mode acp` session actually uses — no longer supersedes its own expiry. Fencing the run aborts the tool that was still executing, and that tool's completion counted as fresh progress, so the expiry backed off and re-armed forever: no terminal frame ever reached the client and the durable record stayed frozen on its pending claim. Progress caused by the expiry's own abort is now discounted, while progress observed before it still protects a genuinely live prompt.
74
+
75
+ - The skill-discovery genuine-emptiness case pins an empty home directory instead of reading the developer's real `~/.claude/skills`. Convention-import diagnostics from a populated home made the notice appear locally and vanish on a clean CI runner, so the case asserted opposite outcomes per machine. The contract it pins is unchanged: an explicit user-scope request that scans nothing and diagnoses nothing carries no `notice`, while the same empty environment under source `all` always scans the four bundled workflow skills and therefore explains the conjunctive filter.
76
+
5
77
  ## [0.17.1] - 2026-09-17
6
78
 
7
79
  ### Fixed
@@ -341,14 +341,15 @@ export declare class AsyncJobManager {
341
341
  * instead of the last-created process-global instance (review thread P1).
342
342
  * Returns TRUE when the mapping was installed (or already held by this
343
343
  * manager); returns FALSE when the endpoint id is already held by a
344
- * FOREIGN live manager — a second top-level session constructed or
345
- * resumed under that endpoint must then fail construction instead of
346
- * silently replacing the first manager, which would make the first
347
- * session's tools resolve the second manager and let same-id jobs be
348
- * queried, registered, or cancelled across sessions (review thread P1). */
344
+ * FOREIGN manager whose authority has not fully settled — a second
345
+ * top-level session constructed or resumed under that endpoint must then
346
+ * fail construction instead of silently replacing the first manager, which
347
+ * would make the first session's tools resolve the second manager and let
348
+ * same-id jobs be queried, registered, or cancelled across sessions
349
+ * (review thread P1). */
349
350
  static registerForEndpoint(endpointId: string, manager: AsyncJobManager): boolean;
350
351
  static forEndpoint(endpointId: string | undefined): AsyncJobManager | undefined;
351
- static unregisterForEndpoint(endpointId: string): void;
352
+ static unregisterForEndpoint(endpointId: string, manager?: AsyncJobManager): void;
352
353
  /** Reverse lookup: the endpoint this manager is registered under, if any
353
354
  * (review thread P1 — endpoint-scoped lineage resolution). */
354
355
  static endpointIdOf(manager: AsyncJobManager | undefined): string | undefined;
@@ -820,6 +820,9 @@ export declare const TIER_MAP_SKIP_LIST: {
820
820
  readonly "opencode-go/qwen3.8-max": {
821
821
  readonly rationale: "post-rebase catalog addition from dev; not yet curated";
822
822
  };
823
+ readonly "opencode-go/union-alpha": {
824
+ readonly rationale: "stealth model; capability tier not yet independently reviewed";
825
+ };
823
826
  readonly "opencode-zen/grok-4.6": {
824
827
  readonly rationale: "post-rebase catalog addition from dev; not yet curated";
825
828
  };
@@ -835,6 +838,9 @@ export declare const TIER_MAP_SKIP_LIST: {
835
838
  readonly "opencode-zen/nemotron-3.5-lightning-free": {
836
839
  readonly rationale: "post-rebase catalog addition from dev; not yet curated";
837
840
  };
841
+ readonly "opencode-zen/union-alpha": {
842
+ readonly rationale: "stealth model; capability tier not yet independently reviewed";
843
+ };
838
844
  readonly "openrouter/anthropic/claude-fable-5:batch": {
839
845
  readonly rationale: "post-rebase catalog addition from dev; not yet curated";
840
846
  };
@@ -6,6 +6,7 @@ export interface FileLockOptions {
6
6
  retryDelayMs?: number;
7
7
  signal?: AbortSignal;
8
8
  onAcquired?: () => void;
9
+ onContended?: () => void;
9
10
  /** Stable host identity required to safely reclaim locks on a shared volume. */
10
11
  ownerHostId?: string;
11
12
  /** Previous local host identities accepted only when deciding stale-owner reclamation. */
@@ -1,5 +1,5 @@
1
1
  import { ThinkingLevel } from "@gajae-code/agent-core";
2
- import type { Api, Model } from "@gajae-code/ai/core";
2
+ import { type Api, type Model } from "@gajae-code/ai/core";
3
3
  import type { AgentSession, DefaultFallbackRuntimeState } from "../session/agent-session";
4
4
  import { type ModelProfileDefinition } from "./model-profiles";
5
5
  export { resolveModelProfileName } from "./model-profile-contract";
@@ -42,7 +42,7 @@ type ConfiguredModelChainState = {
42
42
  };
43
43
  export interface PrepareModelProfileActivationOptions {
44
44
  session: ModelProfileActivationSession;
45
- modelRegistry: Pick<ModelRegistry, "getModelProfile" | "getModelProfiles" | "getAvailableModelProfileNames" | "getApiKeyForProvider" | "getAll" | "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId"> & Partial<Pick<ModelRegistry, "getAvailable" | "getAvailableForProfileActivation" | "resolveModelByLookupAlias" | "lookupAliasExists" | "clearCanonicalVariant" | "seedCanonicalVariant" | "getSessionCanonicalVariant" | "restoreSessionCanonicalVariant" | "getConfiguredProviderIds">> & {
45
+ modelRegistry: Pick<ModelRegistry, "getModelProfile" | "getModelProfiles" | "getAvailableModelProfileNames" | "getApiKeyForProvider" | "getAll" | "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId"> & Partial<Pick<ModelRegistry, "getAvailable" | "getAvailableForProfileActivation" | "resolveModelByLookupAlias" | "lookupAliasExists" | "clearCanonicalVariant" | "seedCanonicalVariant" | "getSessionCanonicalVariant" | "restoreSessionCanonicalVariant" | "getConfiguredProviderIds" | "isKnownProvider">> & {
46
46
  getError?: ModelRegistry["getError"];
47
47
  };
48
48
  settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
@@ -123,13 +123,28 @@ export interface MaterializeModelProfileAssignmentsOptions {
123
123
  export declare function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean;
124
124
  export declare function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean;
125
125
  export declare class ModelProfileCredentialError extends Error {
126
- readonly code = "authentication_failed";
126
+ readonly code: string;
127
127
  readonly profileLabel: string;
128
128
  readonly providers: readonly string[];
129
129
  readonly role: string | undefined;
130
130
  constructor(profileLabel: string, providers: readonly string[], role?: string);
131
131
  }
132
132
  export declare function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string;
133
+ export declare function formatModelProfileUnknownProviderError(profileLabel: string, providers: readonly string[]): string;
134
+ /**
135
+ * A required profile provider that this build cannot know: it is neither a
136
+ * built-in provider id nor declared in models.yml. That is a build/config
137
+ * mismatch (for example a profile authored on a newer or custom build), not a
138
+ * credential gap, so the diagnosis must not send the user to /login.
139
+ *
140
+ * Extends {@link ModelProfileCredentialError} so existing startup recovery
141
+ * (interactive toast-and-continue) keeps working with the sharper message.
142
+ */
143
+ export declare class ModelProfileUnknownProviderError extends ModelProfileCredentialError {
144
+ readonly code = "unknown_provider";
145
+ constructor(profileLabel: string, providers: readonly string[], role?: string);
146
+ }
147
+ export declare function isModelProfileProxyConfigured(provider: string, configuredProviders: readonly string[] | undefined, credentialless: boolean): boolean;
133
148
  /**
134
149
  * Resolve the explicitly configured OpenAI-compatible proxy provider id for a
135
150
  * preset. Returns undefined when unset or empty. Passwords/labels are never
@@ -406,6 +406,15 @@ export declare class ModelRegistry {
406
406
  getAll(): Model<Api>[];
407
407
  /** Provider ids declared in models.yml, including override-only providers. */
408
408
  getConfiguredProviderIds(): readonly string[];
409
+ /**
410
+ * Whether this registry can serve a provider id from any active source.
411
+ *
412
+ * Built-in providers are backed by the bundled AI catalog, configured
413
+ * providers come from models.yml, and extension registrations are retained
414
+ * in the runtime provider stores below. Runtime model/override state is
415
+ * included for direct registrations that do not have an extension source id.
416
+ */
417
+ isKnownProvider(provider: string): boolean;
409
418
  /**
410
419
  * Deterministic provider priority for autorouting tier generation: configured
411
420
  * `modelProviderOrder` first, then first-wins catalog order.
@@ -62,6 +62,9 @@ interface CoordinatorServices {
62
62
  canonicalizePath?: (value: string) => Promise<string>;
63
63
  codexTransportFactory?: CodexTransportFactory;
64
64
  eventWebhookDelivery?: WebhookDelivery;
65
+ /** Observable transitions for consumers coordinating with the filesystem-backed event wait. */
66
+ onCoordinatorEventWatchReady?: () => void;
67
+ onCoordinatorEventWake?: () => void;
65
68
  /** Test barrier invoked after an accepted prompt receipt is durable and before turn finalization. */
66
69
  afterPromptReceiptPersisted?: (sessionId: string) => void | Promise<void>;
67
70
  /** Test barrier invoked after an answer dispatch is claimed and before its final admission check. */
@@ -1,5 +1,6 @@
1
1
  import { type Agent, type AgentSideConnection, type AuthenticateRequest, type AuthenticateResponse, type AvailableCommand, type CancelNotification, type ClientCapabilities, type CloseSessionRequest, type CloseSessionResponse, type DeleteSessionRequest, type DeleteSessionResponse, type ForkSessionRequest, type ForkSessionResponse, type InitializeRequest, type InitializeResponse, type ListSessionsRequest, type ListSessionsResponse, type LoadSessionRequest, type LoadSessionResponse, type NewSessionRequest, type NewSessionResponse, type PromptRequest, type PromptResponse, type ResumeSessionRequest, type ResumeSessionResponse, type SetSessionConfigOptionRequest, type SetSessionConfigOptionResponse, type SetSessionModeRequest, type SetSessionModeResponse } from "@agentclientprotocol/sdk";
2
- import { type AcpProviderRegistration, type AcpReverseConnection, AcpSdkAdapter } from "../../sdk/acp";
2
+ import { type AcpProviderRegistration, type AcpReverseConnection, AcpSdkAdapter, AcpSdkAdapterError } from "../../sdk/acp";
3
+ import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
3
4
  import { type PromptWatchdogClock } from "../../sdk/prompt-watchdog";
4
5
  import type { AcpStartupOptions } from "./startup-options";
5
6
  /**
@@ -43,6 +44,44 @@ export declare function paginateAcpSessions(listed: unknown[], cwd: string | und
43
44
  }>): ListSessionsResponse;
44
45
  /** Build the ACP command palette from the shared builtins and live SDK skill state. */
45
46
  export declare function acpAvailableCommandsFromSkills(query: unknown): AvailableCommand[];
47
+ type SdkPromptFailedOutcome = Extract<SdkPromptTerminalOutcome, {
48
+ kind: "failed";
49
+ }>;
50
+ /**
51
+ * A prompt rejection that still carries the terminal's structured failure classification.
52
+ * The ACP client sees only the code and message, but the first-turn retry gate must decide on
53
+ * what the terminal actually proved — the failure's execution phase and its bounded origin
54
+ * category — so settlement carries that classification instead of discarding it into a bare
55
+ * `AcpSdkAdapterError` (review P1, issue #5574).
56
+ */
57
+ export declare class AcpPromptFailureError extends AcpSdkAdapterError {
58
+ readonly failure: SdkPromptFailedOutcome;
59
+ constructor(failure: SdkPromptFailedOutcome);
60
+ }
61
+ /**
62
+ * What an ACP `plan` update said the turn intended to do. Structural rather than the SDK's
63
+ * `PlanEntry` so only the fields this evidence reads are depended on.
64
+ */
65
+ type AcpPlanSnapshot = Array<{
66
+ content: string;
67
+ status: "pending" | "in_progress" | "completed";
68
+ _meta?: {
69
+ gjcTodoStatus?: unknown;
70
+ } | null;
71
+ }>;
72
+ /**
73
+ * An abandoned prompt that still carries the plan its turn was working through (issue #5669).
74
+ *
75
+ * The watchdog settles a prompt whose host stopped producing frames, and that rejection used to
76
+ * be a bare `AcpSdkAdapterError`: `{code, details}` and nothing else. A wrapper could not tell
77
+ * "the agent finished" from "the agent stopped with work left", so an abandoned turn's unverified
78
+ * diff was published as if it were complete. The last observed plan is the evidence that settles
79
+ * that question, so settlement carries it instead of dropping it at the JSON-RPC boundary.
80
+ */
81
+ export declare class AcpPromptAbandonedError extends AcpSdkAdapterError {
82
+ readonly plan: AcpPlanSnapshot | undefined;
83
+ constructor(code: string, message: string, plan: AcpPlanSnapshot | undefined);
84
+ }
46
85
  export type TranscriptReplayBlock = {
47
86
  type: "text";
48
87
  text: string;
@@ -1,4 +1,5 @@
1
1
  import { type ThinkingLevel as ThinkingLevelValue } from "@gajae-code/agent-core";
2
+ import type { Model } from "@gajae-code/ai/types";
2
3
  import { Container, SelectList } from "@gajae-code/tui";
3
4
  export interface ThinkingSelectorSelection {
4
5
  level: ThinkingLevelValue;
@@ -9,7 +10,7 @@ export interface ThinkingSelectorSelection {
9
10
  */
10
11
  export declare class ThinkingSelectorComponent extends Container {
11
12
  #private;
12
- constructor(currentLevel: ThinkingLevelValue | undefined, availableLevels: ThinkingLevelValue[], onSelect: (selection: ThinkingSelectorSelection) => void, onCancel: () => void);
13
+ constructor(currentLevel: ThinkingLevelValue | undefined, availableLevels: ThinkingLevelValue[], onSelect: (selection: ThinkingSelectorSelection) => void, onCancel: () => void, model?: Model);
13
14
  handleInput(data: string): void;
14
15
  getSelectList(): SelectList;
15
16
  }
@@ -3,6 +3,33 @@ import type { AbortScope } from "../host/control/operations";
3
3
  import { type SessionAttachment, type SessionRouter } from "../router";
4
4
  import { ACP_SESSION_RECONNECT } from "../session-reconnect";
5
5
  import type { SessionLifecycleMcpServer } from "./mcp";
6
+ /**
7
+ * Registration rounds one provider activation may spend chasing a moving attachment.
8
+ *
9
+ * The activation loop restarts whenever the Router attachment changes identity while it
10
+ * is registering, because the leases it just took belong to a connection that no longer
11
+ * exists. One reconnect costs one restart, so a handful covers any churn that is actually
12
+ * converging; beyond that the attachment is rotating faster than registration completes
13
+ * and another round only re-takes leases that will be invalidated again. Bounding this is
14
+ * what turns "many session hosts contend for one agent dir" from an unbounded spin into a
15
+ * reported failure (issue #5658).
16
+ */
17
+ export declare const PROVIDER_ACTIVATION_MAX_ATTEMPTS = 8;
18
+ /**
19
+ * Wall-clock budget for the same loop.
20
+ *
21
+ * The attempt cap alone bounds rounds, not time: every round is a `#requestSession` round
22
+ * trip per provider, so under lease contention attempts are slow rather than fast-spinning
23
+ * and eight of them can still outlast the caller. The budget is sized to a third of
24
+ * {@link ACP_PROMPT_INACTIVITY_TIMEOUT_MS} so a prompt stalled in provider preflight fails
25
+ * with this error — which names the contended capability — with room to spare before the
26
+ * ACP prompt watchdog reports the same stall as undifferentiated silence.
27
+ *
28
+ * It gates *starting* another round, never interrupting one in flight: a single slow but
29
+ * healthy registration keeps whatever time it needs, and only a retry decided after the
30
+ * deadline is refused.
31
+ */
32
+ export declare const PROVIDER_ACTIVATION_BUDGET_MS: number;
6
33
  type JsonObject = Record<string, unknown>;
7
34
  /** The small agent-side ACP surface used for reverse requests. */
8
35
  export interface AcpReverseConnection {
@@ -16,4 +16,12 @@ export type AcpFinalTextResolution = {
16
16
  };
17
17
  export declare function boundAcpFinalText(value: string): BoundedAcpFinalText;
18
18
  export declare function acpFinalTextFromMessage(message: unknown): BoundedAcpFinalText;
19
+ /**
20
+ * Assistant-content presence is trimmed everywhere in the prompt pipeline: whitespace carries no
21
+ * assistant content. The ACP retry gate, prompt reconciliation
22
+ * (`sdk/bus/prompt-reconciliation.ts`) and terminal publication must agree, otherwise a terminal
23
+ * the retry gate treats as "no output" can still publish a whitespace chunk alongside the retry's
24
+ * real answer (review P2 on #5628).
25
+ */
26
+ export declare function hasAcpFinalTextContent(value: string): boolean;
19
27
  export declare function resolveAcpFinalText(streamed: string, finalText: string): AcpFinalTextResolution;
@@ -28,6 +28,51 @@ export interface EnsureBrokerSettings {
28
28
  */
29
29
  env?: NodeJS.ProcessEnv;
30
30
  }
31
+ export interface BrokerStartupLockTestHooks {
32
+ onAcquired?: () => void;
33
+ onContended?: () => void;
34
+ }
35
+ /**
36
+ * Test-only phase observation for detached broker startup regressions. The
37
+ * signal is opt-in through the child environment and never affects broker
38
+ * ownership or error handling when the test seam is absent.
39
+ */
40
+ export declare function emitBrokerStartupTestSignal(signal: string): Promise<void>;
41
+ /**
42
+ * The parent discovery budget published as a composition rather than a number.
43
+ * A test that stages a fence contention has to time itself against these legs,
44
+ * and retyping the sum makes it win or lose on scheduler noise while letting a
45
+ * change to any single leg silently re-tune it (#5604). This is the one source
46
+ * of truth for how the budget is built; nothing may restate the arithmetic.
47
+ */
48
+ export declare const BROKER_DISCOVERY_BUDGET: {
49
+ readonly staleRetirementMs: 5000;
50
+ readonly startupLockWaitMs: 9000;
51
+ readonly publicationMs: 15000;
52
+ /** What the startup fence grants its holder: retirement plus one publication attempt. */
53
+ readonly fenceOperationMs: number;
54
+ /**
55
+ * When a child spawned under the parent's spawn lock gives up on the fence,
56
+ * measured from that lock acquisition: the parent retires the stale incumbent
57
+ * first, then the child exhausts its own fence wait. Not a production
58
+ * constant -- an emergent property of the two legs that precede publication.
59
+ */
60
+ readonly childFenceWaitMs: number;
61
+ readonly discoveryMs: number;
62
+ };
63
+ /**
64
+ * Clock the discovery budget is measured on. The default is the real one; a test
65
+ * installs a virtual clock to assert the composition order -- stale retirement,
66
+ * then the child fence, then startup and publication -- in-process instead of
67
+ * racing three wall clocks against each other (#5604). Reaping keeps its own
68
+ * {@link ReapTiming} seam and is deliberately not driven from here.
69
+ */
70
+ export interface EnsureBrokerTiming {
71
+ now(): number;
72
+ sleep(ms: number): Promise<void>;
73
+ }
74
+ /** Test hook: drives the discovery budget on a controllable clock. */
75
+ export declare function setEnsureBrokerTimingForTest(timing: EnsureBrokerTiming | undefined): void;
31
76
  type SpawnLockOptions = Pick<FileLockOptions, "retries" | "retryDelayMs" | "signal">;
32
77
  declare function readBrokerDiscoveryBeforeDeadline(agentDir: string, heartbeatTtlMs: number | undefined, deadline: number): Promise<BrokerDiscovery | null>;
33
78
  /**
@@ -48,7 +93,7 @@ declare function acquireSpawnLock(agentDir: string, options?: SpawnLockOptions):
48
93
  * The parent-held spawn lock prevents ordinary stampedes; this second fence keeps
49
94
  * startup single-flight if that detached child's launcher dies and its lock is reclaimed.
50
95
  */
51
- export declare function withBrokerStartupLock<T>(agentDir: string, operation: (deadline: number) => Promise<T>): Promise<T>;
96
+ export declare function withBrokerStartupLock<T>(agentDir: string, operation: (deadline: number) => Promise<T>, testHooks?: BrokerStartupLockTestHooks): Promise<T>;
52
97
  /**
53
98
  * Tail of the detached broker's stderr folded into a discovery failure.
54
99
  *
@@ -21,7 +21,7 @@ export declare class PromptDeadlineManager {
21
21
  getLeaseMs: () => number;
22
22
  getMaxMs: () => number;
23
23
  now?: () => number;
24
- onExpired?: (correlation: InvocationCorrelation) => void;
24
+ onExpired?: (correlation: InvocationCorrelation, outcome?: PromptDeadlineOutcome) => void;
25
25
  });
26
26
  onAccepted(correlation: InvocationCorrelation): void;
27
27
  /** Re-arm a durable uncertainty-recovery record after process startup without
@@ -3,7 +3,7 @@
3
3
  * Provider error text is retained only in the local diagnostic log; wire and
4
4
  * persisted reconciliation details expose a fixed redacted message.
5
5
  */
6
- import type { SdkPromptFailureCategory, SdkPromptFailurePhase, SdkPromptTerminalOutcome } from "./prompt-status";
6
+ import type { SdkPromptFailureCategory, SdkPromptFailurePhase, SdkPromptFailureRetryability, SdkPromptTerminalOutcome } from "./prompt-status";
7
7
  export declare const PROMPT_FAILURE_CODE_MAX = 64;
8
8
  export declare const PROMPT_FAILURE_MESSAGE_SUBMISSION = "Prompt submission failed.";
9
9
  export declare const PROMPT_FAILURE_MESSAGE_POST_START_PROVIDER = "Provider failure after execution started.";
@@ -23,6 +23,35 @@ export declare function promptFailurePhase(evidence: PromptFailureEvidence): Sdk
23
23
  export declare function isSdkPromptFailurePhase(value: unknown): value is SdkPromptFailurePhase;
24
24
  /** Allowlisted origin category for a bounded safe classifier + provenance. */
25
25
  export declare function promptFailureCategory(code: string | undefined, provenance: "agent_failed" | "deadline"): SdkPromptFailureCategory;
26
+ /**
27
+ * Whether the bounded category says an identical re-submission could still
28
+ * succeed. Read off the category alone, so it cannot drift from the sets above.
29
+ *
30
+ * `provider_transport` is the only transient class: every classifier in
31
+ * `PROVIDER_TRANSPORT_CODES` (and the 5xx range) describes the link to the
32
+ * provider failing, never a judgement about the request, so the same bytes may
33
+ * well succeed on a second attempt. That is exactly the class the shipped
34
+ * first-turn gate (`isStartupReadinessFailure`, issue #5574) already treats as
35
+ * re-submittable, so the wire signal and the internal gate stay in agreement.
36
+ *
37
+ * Everything else is terminal for this turn: `provider_rejected` is the provider
38
+ * judging this request (quota, refusal, 4xx) and re-sending it reproduces the
39
+ * rejection; `deadline` means the turn's budget is spent, which a re-submit does
40
+ * not restore; `agent_runtime` is a local fault that re-running the same prompt
41
+ * re-enters. `unknown` stays `unknown` — the module's standing contract is to
42
+ * preserve uncertain attribution rather than guess a recovery for it.
43
+ *
44
+ * This is advisory classification for the caller, not a retry gate: nothing in
45
+ * this repo's retry paths consults it.
46
+ */
47
+ export declare function promptFailureRetryability(category: SdkPromptFailureCategory): SdkPromptFailureRetryability;
48
+ /**
49
+ * Whether a value is a safe bounded classifier token — the same rule
50
+ * `sanitizePromptFailure` and `assistantFailureCode` apply before retaining one.
51
+ * Anything else is provider text rather than a classifier and must not be
52
+ * forwarded to a caller.
53
+ */
54
+ export declare function isSafePromptFailureCode(value: unknown): value is string;
26
55
  /**
27
56
  * Phase- and category-aware safe wording. A post-start failure is never
28
57
  * described as a submission rejection; the wording says only what the bounded
@@ -65,6 +65,14 @@ export type SdkPromptFailurePhase = "submission" | "post_start";
65
65
  * attribution is uncertain instead of guessing.
66
66
  */
67
67
  export type SdkPromptFailureCategory = "provider_transport" | "provider_rejected" | "agent_runtime" | "deadline" | "unknown";
68
+ /**
69
+ * Whether the failure class says an identical re-submission could still succeed.
70
+ * Derived from `SdkPromptFailureCategory` alone (see `promptFailureRetryability`)
71
+ * so a consumer never has to re-implement the category matrix or parse a message.
72
+ * `unknown` is preserved rather than collapsed into `terminal`: an uncertain
73
+ * attribution is not evidence that the turn is unrecoverable.
74
+ */
75
+ export type SdkPromptFailureRetryability = "transient" | "terminal" | "unknown";
68
76
  export type SdkPromptTerminalOutcome = {
69
77
  kind: "stopped";
70
78
  reason: SdkPromptStopReason;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Retention-gap concessions are durable stream facts, but a Router can observe
3
+ * many streams over its lifetime. Keep the warning memo bounded while retaining
4
+ * enough recent coordinates to suppress reconnect/poll duplicates.
5
+ */
6
+ export declare const REPLAY_CONCESSION_CACHE_LIMIT = 1024;
7
+ /**
8
+ * Records one stream/gap coordinate in an LRU memo.
9
+ *
10
+ * `undefined` means this is the first observation; a number is the duplicate
11
+ * count after the coordinate was already observed.
12
+ */
13
+ export declare function rememberReplayRetentionGap(cache: Map<string, number>, key: string): number | undefined;
@@ -1,4 +1,31 @@
1
1
  import { SdkClient } from "../client";
2
+ /**
3
+ * A `gjc sdk serve` operational failure with a stable machine-readable code and a
4
+ * documented exit code, mirroring `SdkSessionCliError`. Usage errors keep their own
5
+ * `CliParseError` path; this type covers only failures an embedder must branch on.
6
+ */
7
+ export declare class SdkServeError extends Error {
8
+ readonly code: string;
9
+ readonly exitCode: 1;
10
+ readonly details?: unknown;
11
+ /**
12
+ * Diagnostics for a broker-teardown failure that happened alongside this primary
13
+ * failure. It rides beside `details` rather than inside it because `details`
14
+ * carries the broker's own error payload verbatim and embedders already read it.
15
+ */
16
+ cleanupError?: {
17
+ code: string;
18
+ message: string;
19
+ };
20
+ constructor(code: string, message: string, exitCode: 1, details?: unknown);
21
+ }
22
+ /**
23
+ * Combines the serve body's failure with the broker-teardown failure. The primary
24
+ * failure always wins — a rejected teardown only ever rides along as diagnostics, it
25
+ * never replaces the error an embedder branches on — and a teardown that fails on its
26
+ * own still leaves typed. Exported for tests.
27
+ */
28
+ export declare function resolveServeOutcome(primary: Error | undefined, cleanup: Error | undefined): Error | undefined;
2
29
  /** Resolves the pending ceiling with flag > env > default precedence; exported for tests. */
3
30
  export declare function resolveServePendingCeiling(flagValue: string | undefined, envValue: string | undefined): number;
4
31
  type BrokerSessionRow = {
@@ -1,7 +1,9 @@
1
1
  import { type ResolvedThinkingLevel, ThinkingLevel } from "@gajae-code/agent-core/thinking";
2
2
  import { type Effort } from "@gajae-code/ai/model-thinking";
3
3
  import type { Model } from "@gajae-code/ai/types";
4
- export { getThinkingLevelMetadata, type ThinkingLevelMetadata } from "./thinking-metadata";
4
+ import { type ThinkingLevelMetadata } from "./thinking-metadata";
5
+ export type { ThinkingLevelMetadata } from "./thinking-metadata";
6
+ export declare function getThinkingLevelMetadata(level: ThinkingLevel, model?: Model): ThinkingLevelMetadata;
5
7
  /**
6
8
  * Parses a provider-facing effort value.
7
9
  */
@@ -0,0 +1,2 @@
1
+ import mupdfWasmPath from "../../../../node_modules/mupdf/dist/mupdf-wasm.wasm";
2
+ export default mupdfWasmPath;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.17.1",
4
+ "version": "0.17.2",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -55,12 +55,12 @@
55
55
  "dependencies": {
56
56
  "@agentclientprotocol/sdk": "1.3.0",
57
57
  "@babel/parser": "^7.29.3",
58
- "@gajae-code/agent-core": "0.17.1",
59
- "@gajae-code/ai": "0.17.1",
60
- "@gajae-code/natives": "0.17.1",
61
- "@gajae-code/stats": "0.17.1",
62
- "@gajae-code/tui": "0.17.1",
63
- "@gajae-code/utils": "0.17.1",
58
+ "@gajae-code/agent-core": "0.17.2",
59
+ "@gajae-code/ai": "0.17.2",
60
+ "@gajae-code/natives": "0.17.2",
61
+ "@gajae-code/stats": "0.17.2",
62
+ "@gajae-code/tui": "0.17.2",
63
+ "@gajae-code/utils": "0.17.2",
64
64
  "@mozilla/readability": "^0.6.0",
65
65
  "@puppeteer/browsers": "^2.13.0",
66
66
  "@types/turndown": "5.0.6",