@gajae-code/coding-agent 0.15.3 → 0.15.5

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 (55) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/dist/types/cli/update-cli.d.ts +25 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  4. package/dist/types/config/settings-schema.d.ts +52 -0
  5. package/dist/types/extensibility/extensions/types.d.ts +5 -0
  6. package/dist/types/extensibility/shared-events.d.ts +2 -0
  7. package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
  8. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  9. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  10. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  11. package/dist/types/sdk/host/host.d.ts +2 -0
  12. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  13. package/dist/types/session/agent-session.d.ts +4 -2
  14. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  15. package/dist/types/tools/ask.d.ts +1 -0
  16. package/dist/types/tools/path-utils.d.ts +5 -6
  17. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  18. package/package.json +7 -7
  19. package/src/cli/update-cli.ts +102 -1
  20. package/src/commands/sdk.ts +5 -0
  21. package/src/config/autorouting-tier-map.ts +1 -0
  22. package/src/config/model-profiles.ts +5 -5
  23. package/src/config/model-registry.ts +10 -4
  24. package/src/config/settings-schema.ts +53 -0
  25. package/src/config/settings.ts +1 -1
  26. package/src/coordinator-mcp/policy.ts +1 -1
  27. package/src/deep-interview/render-middleware.ts +6 -2
  28. package/src/extensibility/extensions/types.ts +5 -0
  29. package/src/extensibility/shared-events.ts +2 -0
  30. package/src/gjc-runtime/launch-worktree.ts +44 -7
  31. package/src/gjc-runtime/ralplan-runtime.ts +20 -2
  32. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  33. package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
  34. package/src/gjc-runtime/workflow-placeholder.ts +33 -0
  35. package/src/internal-urls/docs-index.generated.ts +10 -9
  36. package/src/modes/components/hook-selector.ts +2 -1
  37. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  38. package/src/modes/prompt-action-autocomplete.ts +26 -47
  39. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  40. package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
  41. package/src/sdk/bus/prompt-reconciliation.ts +5 -2
  42. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  43. package/src/sdk/cli/session-cli.ts +149 -4
  44. package/src/sdk/host/host.ts +7 -0
  45. package/src/sdk/host/session-runtime.test.ts +821 -22
  46. package/src/sdk/host/session-runtime.ts +669 -67
  47. package/src/sdk/router/session-router.ts +69 -15
  48. package/src/session/agent-session.ts +302 -25
  49. package/src/session/internal/managed-session-storage.ts +64 -26
  50. package/src/session/session-manager.ts +26 -8
  51. package/src/tools/ask-contract.ts +54 -3
  52. package/src/tools/ask.ts +46 -6
  53. package/src/tools/bash.ts +14 -0
  54. package/src/tools/path-utils.ts +77 -29
  55. package/src/tools/tmux-self-injection-guard.ts +359 -0
package/CHANGELOG.md CHANGED
@@ -2,10 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.15.5] - 2026-08-29
6
+
7
+ - Bash now blocks tmux input commands that resolve to the agent's own pane before spawning the shell, including direct pane targets, session aliases, explicit socket forms, shell wrappers, and inspected script indirection. Read-only inspection and cross-session orchestration remain available, while blocked input cannot reach the transcript as a user turn. (#5039)
8
+ - Saved-session staged model-selection appends now re-authenticate metadata-only drift on the same managed transcript object using its unchanged inode, link count, length, and content digest. A changed successor still fails closed, so native Windows persistence no longer turns routine timestamp updates into `managed_append_identity_mismatch` setup failures.
9
+ - The GLM tiers now spend `zai/glm-5.3-flash` on the high-volume lanes and keep judgment roles on `zai/glm-5.3`: `glm-eco` moves default, executor, and planner to `glm-5.3-flash:low` (critic and architect stay `glm-5.3:high`), `glm-medium` swaps only the executor to `glm-5.3-flash:low`, and `glm-pro` swaps the executor to `glm-5.3-flash:high`. GLM-5.3-Flash also joins the bundled `zai` catalog with GLM-5.3's text contract pinned in `applyGeneratedModelPolicy` (1M context, 128K output, always-on `low`/`high`/`max` reasoning effort), because models.dev currently lists it with null limits and a catalog regeneration would otherwise degrade it to the unknown-window fallback; it is skip-listed in the autorouting tier map as not yet curated.
10
+ - A content-free generic OpenAI Responses capacity overload no longer ends the turn under the default retry configuration. OpenAI reports it inside an HTTP 200 terminal envelope (`response.failed`, or `response.completed` with a `failed` status), so the turn surfaced `server_is_overloaded: …` and went idle for a failure the provider says to retry. The bare-default admission now also accepts that exact typed code, read only from transport facts and never from prose: the failed attempt must be on the `openai-responses` API, carry `openaiErrorCode` and `providerCode` equal to `server_is_overloaded` (matched case-sensitively) and no other transport fact (no status, no retained retry header, no second typed code, no error kind, no request size, first-event timing, endpoint class, or provider retry ceiling), and carry no assistant text, thinking, or tool call. Under bare defaults, an overload that follows observable work earlier in the same run is still terminal, `retry.enabled: false` still surfaces immediately, and the Codex and Anthropic overload admissions are unchanged. (#5018)
11
+ - A metadata-only touch of the managed transcript file (AV scanning, indexing, backup sync — anything that bumps `mtimeNs`/`ctimeNs` without changing bytes) no longer crashes an interactive session with an unhandled `managed_append_identity_mismatch` rejection. `appendExpectedIdentitySync` compared only `dev`/`ino`/`size`/`mtimeNs`/`ctimeNs` against the persisted expectation, so a present-but-drifted predecessor was rethrown as a genuine successor and killed the process at the next prompt. The append receipt now carries the content digest the append path already computes (`append_managed` binds `sha256` to the final identity), `#captureManagedPersistIdentity` persists that descriptor-bound digest with the expectation, and a mismatching expectation is re-authenticated: identical bytes on the same unlinked file object with an unchanged link count refresh the expectation and proceed, while a changed inode, length, link count, or digest keeps failing closed so a concurrent successor is never overwritten. Regression-tested with two consecutive drifts, an intact successor guarantee, and an interactive input-loop containment test. (#4892)
12
+ - Behavior change: this overload is now treated as a bounded provider-overload replay under explicit `retry.*` settings as well. It previously fell into the unbounded transient-prose retry class (its message matches `overloaded`), so a configured session retried it without limit; it now stops at `retry.maxRetries` with the existing capped exponential backoff, the same bound bare defaults use. (#5018)
13
+ - Managed fallback behavior is preserved for the typed Responses overload: the new transport facts do not change the agent loop's managed transaction handling or the managed chain's retry/advance policy for this failure (the `server` classification remains a session-side classifier only). (#5018)
14
+ - A single refused notification publication no longer kills a live session's chat mirroring for the rest of that session. `SessionRouter.#dispatchNotificationFrame` treated any rejection from the provider hook as terminal and called `#detachNotification(attached, "cancelled")`, which latches `notificationCancelled` on the `AttachedSession`; only a new attachment clears that latch, and a session that keeps running never gets one. The Telegram daemon throws exactly such a per-frame rejection from `#failPublicationPreSend` (for example `Telegram publication rejected before send: trusted attachment lease is stale`) when its topic or attachment lease momentarily fails `#leaseAllows` while another session attaches or detaches, so one transient authority flip permanently silenced that session's topic while every other session kept delivering — the user-visible symptom being Telegram that "works sometimes". Measured on one install over a single day: 20 such rejections across 20 distinct sessions, exactly one each, and 19 of the 20 never emitted another notification frame afterwards (their topic lease heartbeat stopped one to five seconds after the rejection, because the detach removed the endpoint from the daemon). Dispatch now tolerates a bounded run of consecutive local failures before conceding, mirroring `DELIVERY_ATTEMPT_LIMIT` on the ordered-delivery path in the same file: the refused frame is still dropped (the provider has already settled that publication as rejected on its own side), the next delivered frame resets the run, and a provider that never accepts anything is still cancelled after the limit. The `NOTIFICATION_WORK_TIMEOUT` path is deliberately unchanged — a hung provider is a different failure class and keeps its immediate detach — and core attachment authority, replay, lease renewal, and every other detach reason (`removed`, `replaced`, `replaced_same_generation`) are untouched.
15
+ - `gjc update` now recognizes an already verified standalone binary during package-manager shim migration, avoiding repeated downloads and post-update work while clearly explaining the required PATH ordering.
16
+
17
+ - GJC-managed launch worktrees now default to `<repo>/.worktrees/<slug>` instead of a `<repo>.gajae-code-worktrees` sibling beside the checkout, keeping each repository's isolated branches in one predictable local folder. Launch fails with an actionable diagnostic unless an in-repository bucket is ignored by Git, preventing nested worktrees from polluting the source checkout; external `GJC_WORKTREE_DIR` buckets and already registered sibling worktrees are unchanged.
18
+ - Launch worktree reuse now revalidates Git-registered paths before adoption and reports locked or replaced stale entries instead of running Git commands in an unavailable or occupied directory; coordinator policy and Stream Deck project grouping use the repository-local default while retaining legacy sibling-path recognition.
19
+
20
+ ### Fixed
21
+
22
+ - A one-shot `gjc sdk` command no longer idles for five seconds after its response is written. `SessionRouter` bounded four waits with `Bun.sleep` inside `Promise.race`; the race settles on the winner but does not cancel the loser, and a pending `Bun.sleep` keeps the event loop alive, so work that finished in milliseconds still owed the rest of the budget before the process could exit. `gjc sdk session list` wrote its result at 0.50s and exited at 5.51s. The startup attach budget, shutdown timeout, bounded notification work, and replay backoff now use the existing `cancellableSleep` helper with an `AbortController` cancelled once the race settles, and the reconcile interval is unref'd so a Router started without a matching `stop()` does not hold a host process open. The same command now exits at 1.65s, and every command routed through `withRouter` (`session.list`, `session.create`, `session.delete`, `session.fork`, `turn.prompt`, `turn.result`, `queue.*`, `compaction.*`) is affected.
23
+
24
+ ## [0.15.4] - 2026-08-29
25
+
26
+ - The `ask` tool now rejects empty or whitespace-only custom answers on every local and remote input path. Remote invalid answers receive a visible explanation and are retried at most three times with an event-loop yield between attempts before the ask is cancelled; local empty custom input is never returned as a valid answer. (#5001)
27
+
5
28
  ## [0.15.3] - 2026-08-27
6
29
 
7
30
  ### Added
8
31
 
32
+ - Added opt-in `compaction.adaptive.*` settings and wired them into post-turn and pre-prompt auto-compaction. Adaptive mode remains disabled by default, preserves fixed-token precedence, and exposes bounded base, aggression, call-window, and minimum-threshold controls.
33
+
9
34
  - The `ask` tool declares its display-only argument fields (`questions.question`, `questions.options.label`) via `displaySafeEscapedArgFields`, so an `ask` call whose question text or option labels arrive as `\uXXXX` escapes (common from Anthropic models writing Korean or emoji) completes with a single warning instead of discarding the turn, charging the managed-fallback retry budget, and ultimately failing the run. Ids, workflow-gate metadata, and deep-interview records remain load-bearing and keep the fail-closed rejection. (#4983)
10
35
 
11
36
  - Added an explicit `Copy OAuth URL` command-palette action for interactive `/login` and runtime MCP OAuth flows. It copies the exact pending authorization URL through the configured clipboard transport only after the user selects the action; it never overwrites the clipboard automatically and clears the pending URL when the flow ends. This preserves OSC 8 policy, tmux/SSH/no-hyperlink behavior, and avoids relying on padded soft-wrapped terminal rows for copying.
@@ -35,6 +60,7 @@
35
60
  - Foreground activity animation now uses layout-only TUI repaints, avoiding repeated reconstruction of unchanged transcript content during long sessions.
36
61
  - Notification adapters can now opt into one live representation per native effect: adapters that accept a positioned event are excluded from its matching raw fan-out, while ordinary direct SDK and raw-only legacy subscribers retain their existing delivery. Telegram opts in, preventing turn output, tool activity, and reasoning summaries from rendering twice after #4570; acknowledged terminal shutdown delivery is unchanged.
37
62
  - Lean notifications no longer replay a retained completion receipt at idle when identical text was already delivered immediately before an autonomous `ask`; distinct receipts and other settlement windows remain preserved.
63
+ - SDK prompts now publish a bounded structured terminal failure when a provider immediately returns HTTP 402 or 429. The accepted receipt remains non-terminal until the correlated `agent_end`, while `turn.result`/`turn.prompt_status` no longer strand the prompt as `in_flight`; replacement turns remain independently abortable and recoverable. (#4941)
38
64
 
39
65
  ## [0.15.2] - 2026-08-25
40
66
 
@@ -43,7 +69,6 @@
43
69
  - Version 0.15.1 was tagged but never published: release automation failed while deriving release notes, before any package reached npm. Everything listed under `## [0.15.1]` below ships in this release.
44
70
 
45
71
  ## [0.15.1] - 2026-08-25
46
-
47
72
  ### Added
48
73
 
49
74
  - Added `/language [en|ko]`, the interactive slash command for the persisted `ui.language` selection. Without arguments it reports the current language; with a canonical code, locale tag (`en-US`, `ko-KR`), endonym (`한국어`), English name, or common aliases (`eng`, `kr`, `kor`) it persists the canonical `en`/`ko` value through settings and confirms in the selected language. An unsupported value is rejected with the available list and changes nothing. See `docs/ui-language.md`.
@@ -53,6 +53,11 @@ export type PackageManagerTarget = {
53
53
  manager: "npm";
54
54
  packageName: string;
55
55
  };
56
+ type MigrationUpdateTarget = {
57
+ method: "migrate";
58
+ path: string;
59
+ previousPath?: string;
60
+ };
56
61
  export type UpdateTarget = {
57
62
  method: "bun";
58
63
  } | {
@@ -61,11 +66,7 @@ export type UpdateTarget = {
61
66
  } | {
62
67
  method: "binary";
63
68
  path: string;
64
- } | {
65
- method: "migrate";
66
- path: string;
67
- previousPath?: string;
68
- };
69
+ } | MigrationUpdateTarget;
69
70
  type PathPlatform = NodeJS.Platform;
70
71
  type PackageExists = (packageName: string, packageRoot: string) => boolean;
71
72
  export declare function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary";
@@ -85,6 +86,24 @@ export declare function resolveGjcPathForTest(options: {
85
86
  whichPath: string | undefined;
86
87
  }): string | undefined;
87
88
  export declare function parseReportedVersionForTest(output: string): string | undefined;
89
+ interface MigrationTargetVerificationOptions {
90
+ runtimePath: string;
91
+ verifyChecksum: () => Promise<void>;
92
+ verifyRuntime: () => Promise<InstalledVersionVerification>;
93
+ }
94
+ interface MigrationChecksumOptions {
95
+ tag: string;
96
+ assetName: string;
97
+ filePath: string;
98
+ }
99
+ type MigrationChecksumVerifier = (options: MigrationChecksumOptions) => Promise<unknown>;
100
+ export declare function verifyMigrationTargetForTest(options: MigrationTargetVerificationOptions): Promise<InstalledVersionVerification>;
101
+ export declare function verifyMigrationTargetAdapterForTest(options: {
102
+ release: Pick<ReleaseInfo, "tag" | "version">;
103
+ runtimePath: string;
104
+ verifyChecksum: MigrationChecksumVerifier;
105
+ verifyRuntime: (expectedVersion: string, runtimePath: string) => Promise<InstalledVersionVerification>;
106
+ }): Promise<InstalledVersionVerification>;
88
107
  export declare function formatBinaryDownloadFailureMessageForTest(binaryName: string, url: string, status: string | number, platform?: NodeJS.Platform, registryNote?: string): string;
89
108
  export declare function buildReleaseBinaryUrlForTest(version: string, platform?: NodeJS.Platform, arch?: string): string;
90
109
  export declare function formatManualUpdateInstructionsForTest(platform?: NodeJS.Platform): string;
@@ -123,6 +142,7 @@ export declare function runBinaryUpdateFlow(targetPath: string, url: string, exp
123
142
  export interface UpdateCommandDependencies {
124
143
  getLatestRelease?: (options?: LatestReleaseLookupOptions) => Promise<ReleaseInfo>;
125
144
  resolveUpdateTarget?: () => Promise<UpdateTarget>;
145
+ verifyMigrationTarget?: (release: ReleaseInfo, runtimePath: string) => Promise<InstalledVersionVerification>;
126
146
  performUpdate?: (target: UpdateTarget, expectedVersion: string, registry?: string) => Promise<InstalledVersionVerification | undefined>;
127
147
  refreshInstalledDefaultSkills?: () => Promise<void>;
128
148
  settings?: () => Promise<Settings>;
@@ -250,6 +250,9 @@ export declare const TIER_MAP_SKIP_LIST: {
250
250
  readonly "zai/glm-5.3": {
251
251
  readonly rationale: "post-rebase catalog addition from dev; not yet curated";
252
252
  };
253
+ readonly "zai/glm-5.3-flash": {
254
+ readonly rationale: "post-feature catalog addition; not yet curated";
255
+ };
253
256
  readonly "cursor/gemini-3.7-flash-high": {
254
257
  readonly rationale: "post-rebase catalog addition from dev; not yet curated";
255
258
  };
@@ -2193,6 +2193,51 @@ export declare const SETTINGS_SCHEMA: {
2193
2193
  }];
2194
2194
  };
2195
2195
  };
2196
+ readonly "compaction.adaptive.enabled": {
2197
+ readonly type: "boolean";
2198
+ readonly default: false;
2199
+ readonly ui: {
2200
+ readonly tab: "context";
2201
+ readonly label: "Adaptive Compaction";
2202
+ readonly description: "Adjust compaction frequency by context size and call rate";
2203
+ };
2204
+ };
2205
+ readonly "compaction.adaptive.baseThresholdPercent": {
2206
+ readonly type: "number";
2207
+ readonly default: 85;
2208
+ readonly ui: {
2209
+ readonly tab: "context";
2210
+ readonly label: "Adaptive Base Threshold %";
2211
+ readonly description: "Base context percentage used when adaptive compaction is enabled";
2212
+ };
2213
+ };
2214
+ readonly "compaction.adaptive.aggression": {
2215
+ readonly type: "number";
2216
+ readonly default: 0.15;
2217
+ readonly ui: {
2218
+ readonly tab: "context";
2219
+ readonly label: "Adaptive Aggression";
2220
+ readonly description: "How strongly call rate lowers the compaction threshold; use 0 to 1";
2221
+ };
2222
+ };
2223
+ readonly "compaction.adaptive.turnWindow": {
2224
+ readonly type: "number";
2225
+ readonly default: 15;
2226
+ readonly ui: {
2227
+ readonly tab: "context";
2228
+ readonly label: "Call-Rate Window";
2229
+ readonly description: "Minutes of recent calls considered by adaptive compaction";
2230
+ };
2231
+ };
2232
+ readonly "compaction.adaptive.minThresholdPercent": {
2233
+ readonly type: "number";
2234
+ readonly default: 50;
2235
+ readonly ui: {
2236
+ readonly tab: "context";
2237
+ readonly label: "Adaptive Floor %";
2238
+ readonly description: "Lowest context percentage adaptive compaction may lower the threshold to";
2239
+ };
2240
+ };
2196
2241
  readonly "compaction.handoffSaveToDisk": {
2197
2242
  readonly type: "boolean";
2198
2243
  readonly default: false;
@@ -4521,6 +4566,13 @@ export interface CompactionSettings {
4521
4566
  strategy: "context-full" | "handoff" | "off";
4522
4567
  thresholdPercent: number;
4523
4568
  thresholdTokens: number;
4569
+ adaptive: {
4570
+ enabled: boolean;
4571
+ baseThresholdPercent: number;
4572
+ aggression: number;
4573
+ turnWindow: number;
4574
+ minThresholdPercent: number;
4575
+ };
4524
4576
  reserveTokens: number;
4525
4577
  keepRecentTokens: number;
4526
4578
  handoffSaveToDisk: boolean;
@@ -80,6 +80,8 @@ export interface ExtensionUIDialogOptions {
80
80
  customInput?: {
81
81
  optionLabel: string;
82
82
  onSubmit: (text: string) => void;
83
+ /** Empty/whitespace-only submissions may be rejected by the selector. */
84
+ allowEmpty?: boolean;
83
85
  };
84
86
  /**
85
87
  * Inline free-text input for a non-answer clarification action. It is
@@ -342,6 +344,7 @@ export interface ExtensionContext {
342
344
  cleanedArgs?: string;
343
345
  }) => void;
344
346
  preflightSignal?: AbortSignal;
347
+ sdkRunToken?: string;
345
348
  }): Promise<unknown>;
346
349
  setPlanMode?(on: boolean): unknown;
347
350
  operateGoal?(op: "create" | "get" | "resume" | "pause" | "complete" | "drop", objective?: string): Promise<unknown>;
@@ -490,6 +493,8 @@ export interface AgentStartEvent extends SharedAgentStartEvent {
490
493
  export interface AgentFailedEvent {
491
494
  type: "agent_failed";
492
495
  error: AgentFailureDiagnostic;
496
+ /** Internal SDK queue-owner binding for exact lifecycle attribution. */
497
+ sdkRunToken?: string;
493
498
  /** Attempt correlation for the failing run, when scoped (matches the
494
499
  * agent_start/agent_end scope contract). */
495
500
  scope?: AttemptScope;
@@ -148,6 +148,8 @@ export interface AgentStartEvent {
148
148
  export interface AgentEndEvent {
149
149
  type: "agent_end";
150
150
  messages: AgentMessage[];
151
+ /** Internal SDK queue-owner binding for exact lifecycle attribution. */
152
+ sdkRunToken?: string;
151
153
  /** Indicates whether the loop ended normally, suspended, cancelled, or entered maintenance. */
152
154
  stopReason?: "completed" | "paused" | "cancelled" | "maintenance";
153
155
  /** Present for maintenance checkpoints; non-aborted checkpoints are not terminal. */
@@ -0,0 +1,6 @@
1
+ /** Shared placeholder semantics for model-authored workflow content. */
2
+ export declare const WORKFLOW_PLACEHOLDER_CORRECTION = "provide a specific, non-placeholder question or objective instead of empty, whitespace, unused, TODO, TBD, N/A, N-A, NA, none, placeholder, or stub";
3
+ /** Exact, case-insensitive placeholder detection; meaningful sentences remain valid. */
4
+ export declare function isWorkflowPlaceholderText(value: unknown): boolean;
5
+ /** Detect a legacy formatted deep-interview ask whose visible body is a placeholder. */
6
+ export declare function isLegacyDeepInterviewPlaceholder(value: unknown): boolean;
@@ -47,6 +47,7 @@ export interface HookSelectorOptions {
47
47
  customInput?: {
48
48
  optionLabel: string;
49
49
  onSubmit: (text: string) => void;
50
+ allowEmpty?: boolean;
50
51
  };
51
52
  clarificationInput?: {
52
53
  optionLabel: string;
@@ -278,8 +278,15 @@ export declare const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
278
278
  * Generation 177 consumes an exact same-window lean settlement receipt only
279
279
  * after its autonomous ask lead-in publishes successfully, preventing idle
280
280
  * replay without changing the serving protocol.
281
+ * Generation 178: idle publication waits for positioned identity delivery to
282
+ * cross the native writer barrier before the independent broadcast lane.
283
+ * Generation 179 tolerates a bounded consecutive run of refused notification
284
+ * publications in the router instead of cancelling the subscription on the
285
+ * first refusal, so an owner still running the generation-178 router keeps
286
+ * killing a session's mirroring after one transient rejection and must be
287
+ * replaced across this upgrade.
281
288
  */
282
- export declare const DAEMON_GENERATION = 178;
289
+ export declare const DAEMON_GENERATION = 179;
283
290
  /**
284
291
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
285
292
  * requires durable publication receipts and acknowledged attachment dispatch,
@@ -1,5 +1,7 @@
1
1
  import { SessionRouter } from "../router";
2
- export type SdkSessionCliAction = "list" | "inspect" | "send" | "status" | "tail" | "retire" | "raw" | "control" | "query" | "global";
2
+ import { type SdkSessionRowV1 } from "./rows";
3
+ export type SdkSessionCliAction = "list" | "inspect" | "send" | "status" | "tail" | "retire" | "global" | "raw" | "control" | "query" | "global";
4
+ export type SdkSessionListScope = "repo" | "cwd" | "worktree" | "all";
3
5
  export type SdkSessionCliRawKind = "control" | "query" | "global";
4
6
  export interface SdkSessionCliArgs {
5
7
  action?: string;
@@ -13,6 +15,7 @@ export interface SdkSessionCliArgs {
13
15
  jsonInputFile?: string;
14
16
  jsonInputStdin?: boolean;
15
17
  idempotencyKey?: string;
18
+ scope?: string;
16
19
  confirm?: boolean;
17
20
  cursor?: string;
18
21
  wait?: boolean;
@@ -27,6 +30,43 @@ export interface RetainedTranscriptTailReader {
27
30
  readonly size: number;
28
31
  readRange(start: number, end: number): Promise<Uint8Array>;
29
32
  }
33
+ interface WorkspaceIdentity {
34
+ /** Canonical workspace path (realpath when it exists, lexical resolve otherwise). */
35
+ canonicalPath: string;
36
+ /** Canonical containing worktree root; null outside a Git checkout. */
37
+ repoRoot: string | null;
38
+ /** Canonical Git common dir; shared by the main checkout and linked worktrees. */
39
+ commonDir: string | null;
40
+ }
41
+ export interface SdkSessionListSelection {
42
+ scope: SdkSessionListScope;
43
+ selection: WorkspaceIdentity;
44
+ /** Bounded, credential-free descriptor included in list output. */
45
+ descriptor: {
46
+ scope: SdkSessionListScope;
47
+ path: string;
48
+ worktreeRoot?: string;
49
+ commonDir?: string;
50
+ };
51
+ }
52
+ /** Parses `--scope`; missing means `repo`, anything else invalid is a usage error. */
53
+ export declare function parseSessionListScope(value: string | undefined): SdkSessionListScope;
54
+ /**
55
+ * Resolves the list selection from `--repo` (default: process cwd) under the
56
+ * effective scope. Outside Git, `repo`/`worktree` fail typed and actionable —
57
+ * they never broaden to `all`; `cwd` remains an exact canonical match.
58
+ */
59
+ export declare function resolveSessionListSelection(scope: SdkSessionListScope, repoArg: string | undefined): Promise<SdkSessionListSelection>;
60
+ /**
61
+ * Filters fully traversed broker rows by the selection scope. Row workspaces
62
+ * are canonicalized and cached per distinct locator; `repo` matches the shared
63
+ * common dir across the main checkout and linked worktrees, `worktree` the
64
+ * containing worktree only, and `cwd` the exact canonical workspace.
65
+ */
66
+ export declare function filterSessionRowsByScope(rows: readonly SdkSessionRowV1[], scope: SdkSessionListScope, selection: SdkSessionListSelection): Promise<{
67
+ sessions: SdkSessionRowV1[];
68
+ warnings: string[];
69
+ }>;
30
70
  /** Creates a lowercase ULID operation reference for prompt reconciliation. */
31
71
  export declare function createOperationRef(now?: number): string;
32
72
  /** Exported for tests: each status poll must stay inside the caller's wait window. */
@@ -39,3 +79,4 @@ export declare function scanRetainedTranscriptTail(reader: RetainedTranscriptTai
39
79
  export declare function runTail(repo: string, agentDir: string, sessionId: string, args: SdkSessionCliArgs): Promise<unknown>;
40
80
  /** Runs the broker-bound `gjc sdk session` command family without exposing endpoint credentials. */
41
81
  export declare function runSdkSessionCli(args: SdkSessionCliArgs, writeOutput?: (value: unknown) => void, setExitCode?: (exitCode: 1 | 2) => void): Promise<void>;
82
+ export {};
@@ -36,6 +36,8 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
36
36
  query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
37
37
  /** Test/lifecycle seam invoked synchronously before fire-and-forget dispatch. */
38
38
  onFrameAdmitted?: (connectionId: string, frame: SdkFrame) => void;
39
+ /** Synchronously rejects external input while a runtime is being replaced. */
40
+ onFrameAdmission?: (connectionId: string, frame: SdkFrame) => SdkFrame | undefined;
39
41
  /** Best-effort diagnostic observation of accepted control/query frames. */
40
42
  onRequest?: SdkRequestObserver;
41
43
  /** Runs before a control response is sent; identity transitions use sendTerminal. */
@@ -175,6 +175,10 @@ export interface CreateSdkSessionRuntimeOptions {
175
175
  terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
176
176
  /** Callback when a frame is admitted to the runtime (test harness). */
177
177
  onFrameAdmitted?: () => void;
178
+ /** Test-only observation of a genuinely timed-out lifecycle drain. */
179
+ onLifecycleDrainTimeoutForTests?: () => void;
180
+ /** Test-only observation of bounded failure-diagnostic deduplication state. */
181
+ onFailureDiagnosticKeyCountForTests?: (count: number) => void;
178
182
  }
179
183
  export interface InvocationCorrelation {
180
184
  commandId: string;
@@ -426,6 +426,8 @@ export interface PromptOptions {
426
426
  }) => void;
427
427
  /** Optional invocation-scoped cancellation fence used before an accepted skill starts execution. */
428
428
  preflightSignal?: AbortSignal;
429
+ /** Internal SDK lifecycle owner token. */
430
+ sdkRunToken?: string;
429
431
  }
430
432
  /** Result from a handoff operation. */
431
433
  export interface HandoffResult {
@@ -945,7 +947,7 @@ export declare class AgentSession {
945
947
  /** Live SDK configuration values exposed through the session query surface. */
946
948
  getSdkConfigItems(): Record<string, string>;
947
949
  setPlanModeState(state: PlanModeState | undefined): void;
948
- invokeSkill(name: string, args?: string, options?: Pick<PromptOptions, "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal">): Promise<{
950
+ invokeSkill(name: string, args?: string, options?: Pick<PromptOptions, "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunToken">): Promise<{
949
951
  name: string;
950
952
  path: string;
951
953
  args?: string;
@@ -1000,7 +1002,7 @@ export declare class AgentSession {
1000
1002
  * @throws Error if no model selected or no API key available (when not streaming)
1001
1003
  */
1002
1004
  prompt(text: string, options?: PromptOptions): Promise<void>;
1003
- promptCustomMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">, options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted" | "onPreflightAcceptCommit" | "preflightSignal">): Promise<void>;
1005
+ promptCustomMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">, options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted" | "onPreflightAcceptCommit" | "preflightSignal" | "sdkRunToken">): Promise<void>;
1004
1006
  /**
1005
1007
  * Queue a steering message to interrupt the agent mid-run.
1006
1008
  */
@@ -65,9 +65,8 @@ export interface ManagedFileSnapshot {
65
65
  };
66
66
  }
67
67
  /**
68
- * Descriptor identity of one managed file object. Deliberately excludes the
69
- * content sha256: append receipts must bind the post-operation object without
70
- * re-reading the whole payload.
68
+ * Descriptor identity of one managed file object. Append receipts include a
69
+ * content digest when the append implementation has already computed it.
71
70
  */
72
71
  export interface ManagedFileIdentity {
73
72
  dev: bigint;
@@ -76,6 +75,7 @@ export interface ManagedFileIdentity {
76
75
  size: number;
77
76
  mtimeNs: bigint;
78
77
  ctimeNs: bigint;
78
+ sha256?: string;
79
79
  }
80
80
  /**
81
81
  * Post-operation evidence for one managed append. `descriptor` is the
@@ -91,6 +91,7 @@ export interface ManagedAppendReceipt {
91
91
  export interface ManagedBoundedAppendExpectation {
92
92
  readonly dev: string;
93
93
  readonly ino: string;
94
+ readonly nlink: string;
94
95
  readonly size: string;
95
96
  readonly mtimeNs: string;
96
97
  readonly ctimeNs: string;
@@ -45,6 +45,7 @@ export interface AskToolDetails {
45
45
  export declare const OTHER_OPTION = "Other (type your own)";
46
46
  export declare const ASK_CLARIFICATION_OPTION = "Ask about these choices";
47
47
  export declare const RECOMMENDED_SUFFIX = " (Recommended)";
48
+ export declare const MAX_EMPTY_CUSTOM_ATTEMPTS = 3;
48
49
  /** The only remote navigation control; labels are presentation, never protocol. */
49
50
  export declare function askRemoteControls(input: {
50
51
  multi: boolean;
@@ -8,12 +8,11 @@ export declare function splitPathAndSel(rawPath: string): {
8
8
  /**
9
9
  * Variant of {@link splitPathAndSel} for internal URLs (`scheme://...`).
10
10
  *
11
- * Artifact authorities are generated identifiers, so their complete tail is
12
- * unambiguously a selector and malformed selectors can be rejected before
13
- * resolving the artifact. Other resources may legitimately use colons in
14
- * their identities, so they only lose strict, recognized selector suffixes.
15
- * Skill authorities additionally use the active registry to distinguish a
16
- * skill name from its selector without making path-utils depend on skills.
11
+ * Selector-capable internal authorities use identifier-shaped resource names,
12
+ * so an authority-only `:<tail>` is an explicit read selector even when the
13
+ * selector is malformed. Skill authorities are the exception: namespaced
14
+ * skill names already contain a colon, so the active registry gets first
15
+ * refusal and the fallback recognizes a later colon as the selector boundary.
17
16
  */
18
17
  export declare function splitInternalUrlSel(rawPath: string, options?: {
19
18
  activeSkillNames?: readonly string[];
@@ -0,0 +1,15 @@
1
+ export interface TmuxSelfInjectionResult {
2
+ block: boolean;
3
+ reason?: string;
4
+ }
5
+ export type TmuxPaneResolver = (options: {
6
+ socketArgs: string[];
7
+ target: string;
8
+ env: Record<string, string>;
9
+ }) => Promise<string | undefined>;
10
+ export interface TmuxSelfInjectionOptions {
11
+ env?: Record<string, string | undefined>;
12
+ cwd?: string;
13
+ resolvePaneId?: TmuxPaneResolver;
14
+ }
15
+ export declare function checkTmuxSelfInjection(command: string, options?: TmuxSelfInjectionOptions, depth?: number): Promise<TmuxSelfInjectionResult>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.15.3",
4
+ "version": "0.15.5",
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",
@@ -56,12 +56,12 @@
56
56
  "@agentclientprotocol/sdk": "1.3.0",
57
57
  "@babel/parser": "^7.29.3",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "@gajae-code/stats": "0.15.3",
60
- "@gajae-code/agent-core": "0.15.3",
61
- "@gajae-code/ai": "0.15.3",
62
- "@gajae-code/natives": "0.15.3",
63
- "@gajae-code/tui": "0.15.3",
64
- "@gajae-code/utils": "0.15.3",
59
+ "@gajae-code/stats": "0.15.5",
60
+ "@gajae-code/agent-core": "0.15.5",
61
+ "@gajae-code/ai": "0.15.5",
62
+ "@gajae-code/natives": "0.15.5",
63
+ "@gajae-code/tui": "0.15.5",
64
+ "@gajae-code/utils": "0.15.5",
65
65
  "@puppeteer/browsers": "^2.13.0",
66
66
  "@types/turndown": "5.0.6",
67
67
  "@xterm/headless": "^6.0.0",
@@ -163,11 +163,12 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
163
163
  }
164
164
 
165
165
  export type PackageManagerTarget = { manager: "npm"; packageName: string };
166
+ type MigrationUpdateTarget = { method: "migrate"; path: string; previousPath?: string };
166
167
  export type UpdateTarget =
167
168
  | { method: "bun" }
168
169
  | { method: "npm"; packageName: string }
169
170
  | { method: "binary"; path: string }
170
- | { method: "migrate"; path: string; previousPath?: string };
171
+ | MigrationUpdateTarget;
171
172
 
172
173
  type PathPlatform = NodeJS.Platform;
173
174
  type PackageExists = (packageName: string, packageRoot: string) => boolean;
@@ -506,6 +507,73 @@ async function verifyInstalledRuntime(
506
507
  }
507
508
  }
508
509
 
510
+ interface MigrationTargetVerificationOptions {
511
+ runtimePath: string;
512
+ verifyChecksum: () => Promise<void>;
513
+ verifyRuntime: () => Promise<InstalledVersionVerification>;
514
+ }
515
+
516
+ interface MigrationChecksumOptions {
517
+ tag: string;
518
+ assetName: string;
519
+ filePath: string;
520
+ }
521
+
522
+ type MigrationChecksumVerifier = (options: MigrationChecksumOptions) => Promise<unknown>;
523
+
524
+ async function verifyMigrationTargetWith(
525
+ options: MigrationTargetVerificationOptions,
526
+ ): Promise<InstalledVersionVerification> {
527
+ try {
528
+ await options.verifyChecksum();
529
+ } catch {
530
+ return { ok: false, path: options.runtimePath };
531
+ }
532
+ return await options.verifyRuntime();
533
+ }
534
+
535
+ async function verifyMigrationTarget(
536
+ release: Pick<ReleaseInfo, "tag" | "version">,
537
+ runtimePath: string,
538
+ verifyChecksum: MigrationChecksumVerifier = verifyDownloadedBinaryChecksum,
539
+ verifyRuntime: (
540
+ expectedVersion: string,
541
+ runtimePath: string,
542
+ ) => Promise<InstalledVersionVerification> = verifyInstalledRuntime,
543
+ ): Promise<InstalledVersionVerification> {
544
+ return await verifyMigrationTargetWith({
545
+ runtimePath,
546
+ verifyChecksum: async () => {
547
+ await verifyChecksum({
548
+ tag: release.tag,
549
+ assetName: getBinaryName(),
550
+ filePath: runtimePath,
551
+ });
552
+ },
553
+ verifyRuntime: async () => await verifyRuntime(release.version, runtimePath),
554
+ });
555
+ }
556
+
557
+ export async function verifyMigrationTargetForTest(
558
+ options: MigrationTargetVerificationOptions,
559
+ ): Promise<InstalledVersionVerification> {
560
+ return await verifyMigrationTargetWith(options);
561
+ }
562
+
563
+ export async function verifyMigrationTargetAdapterForTest(options: {
564
+ release: Pick<ReleaseInfo, "tag" | "version">;
565
+ runtimePath: string;
566
+ verifyChecksum: MigrationChecksumVerifier;
567
+ verifyRuntime: (expectedVersion: string, runtimePath: string) => Promise<InstalledVersionVerification>;
568
+ }): Promise<InstalledVersionVerification> {
569
+ return await verifyMigrationTarget(
570
+ options.release,
571
+ options.runtimePath,
572
+ options.verifyChecksum,
573
+ options.verifyRuntime,
574
+ );
575
+ }
576
+
509
577
  function printRestartGuidance(): void {
510
578
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
511
579
  }
@@ -1062,6 +1130,7 @@ async function updateViaBinaryAt(
1062
1130
  export interface UpdateCommandDependencies {
1063
1131
  getLatestRelease?: (options?: LatestReleaseLookupOptions) => Promise<ReleaseInfo>;
1064
1132
  resolveUpdateTarget?: () => Promise<UpdateTarget>;
1133
+ verifyMigrationTarget?: (release: ReleaseInfo, runtimePath: string) => Promise<InstalledVersionVerification>;
1065
1134
  performUpdate?: (
1066
1135
  target: UpdateTarget,
1067
1136
  expectedVersion: string,
@@ -1265,6 +1334,24 @@ export function resolveUpdateDecision(options: {
1265
1334
  return { install: true, kind: options.comparison > 0 ? "new-version" : "force" };
1266
1335
  }
1267
1336
 
1337
+ function printVerifiedMigrationTarget(target: MigrationUpdateTarget, version: string): void {
1338
+ console.log(
1339
+ chalk.green(
1340
+ `${theme.status.success} Standalone ${APP_NAME} ${version} is already installed and verified at ${target.path}`,
1341
+ ),
1342
+ );
1343
+ if (target.previousPath) {
1344
+ console.log(chalk.yellow(`${target.previousPath} shadows it on PATH.`));
1345
+ console.log(
1346
+ chalk.cyan(
1347
+ `The standalone directory ${path.dirname(target.path)} must precede the shim directory ${path.dirname(target.previousPath)} on PATH.`,
1348
+ ),
1349
+ );
1350
+ return;
1351
+ }
1352
+ console.log(chalk.cyan(`Ensure the standalone directory ${path.dirname(target.path)} is on PATH.`));
1353
+ }
1354
+
1268
1355
  export async function runUpdateCommand(
1269
1356
  opts: UpdateCommandOptions,
1270
1357
  deps: UpdateCommandDependencies = {},
@@ -1272,6 +1359,7 @@ export async function runUpdateCommand(
1272
1359
  const channel = opts.channel ?? "stable";
1273
1360
  const lookupRelease = deps.getLatestRelease ?? getLatestRelease;
1274
1361
  const resolveTarget = deps.resolveUpdateTarget ?? resolveUpdateTarget;
1362
+ const verifyTarget = deps.verifyMigrationTarget ?? verifyMigrationTarget;
1275
1363
  const update = deps.performUpdate ?? performUpdate;
1276
1364
  const refreshDefaults = deps.refreshInstalledDefaultSkills ?? refreshInstalledDefaultSkills;
1277
1365
  const exit = deps.exit ?? process.exit;
@@ -1323,6 +1411,19 @@ export async function runUpdateCommand(
1323
1411
  migrate: target?.method === "migrate",
1324
1412
  });
1325
1413
 
1414
+ if (target.method === "migrate" && decision.install && !opts.force) {
1415
+ const releaseLock = await acquireBinaryUpdateLock(target.path);
1416
+ try {
1417
+ const verification = await verifyTarget(release, target.path);
1418
+ if (verification.ok) {
1419
+ printVerifiedMigrationTarget(target, release.version);
1420
+ return;
1421
+ }
1422
+ } finally {
1423
+ await releaseLock();
1424
+ }
1425
+ }
1426
+
1326
1427
  if (!decision.install) {
1327
1428
  console.log(chalk.green(`${theme.status.success} Already up to date`));
1328
1429
  return;