@gajae-code/coding-agent 0.12.8 → 0.12.10

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 (40) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/sdk.d.ts +23 -0
  4. package/dist/types/discovery/agents-md.d.ts +11 -1
  5. package/dist/types/modes/types.d.ts +6 -0
  6. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  7. package/dist/types/sdk/bus/notification-service.d.ts +23 -0
  8. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  9. package/dist/types/session/internal/managed-session-storage.d.ts +8 -0
  10. package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
  11. package/dist/types/system-prompt.d.ts +12 -1
  12. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  13. package/package.json +8 -8
  14. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  15. package/src/cli/notify-cli.ts +12 -6
  16. package/src/commands/sdk.ts +54 -0
  17. package/src/config/model-profiles.ts +8 -0
  18. package/src/config/model-registry.ts +4 -1
  19. package/src/discovery/agents-md.ts +76 -19
  20. package/src/gjc-runtime/psmux-detect.ts +12 -16
  21. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  22. package/src/internal-urls/docs-index.generated.ts +4 -3
  23. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  24. package/src/modes/controllers/selector-controller.ts +61 -43
  25. package/src/modes/print-mode.ts +3 -0
  26. package/src/modes/types.ts +6 -0
  27. package/src/prompts/system/system-prompt.md +1 -0
  28. package/src/sdk/bus/daemon-paths.ts +2 -0
  29. package/src/sdk/bus/notification-service.ts +140 -7
  30. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  31. package/src/sdk/session.ts +24 -7
  32. package/src/session/agent-session.ts +65 -13
  33. package/src/session/internal/managed-session-storage.ts +36 -2
  34. package/src/session/internal/native-publish-outcome.ts +12 -2
  35. package/src/slash-commands/builtin-registry.ts +35 -11
  36. package/src/system-prompt.ts +28 -15
  37. package/src/tools/bash.ts +3 -1
  38. package/src/tools/composer-bash-policy.ts +21 -5
  39. package/src/utils/command-args.ts +19 -30
  40. package/src/utils/lang-from-path.ts +2 -4
package/CHANGELOG.md CHANGED
@@ -2,10 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.10] - 2026-08-03
6
+
7
+ ## [0.12.9] - 2026-08-03
8
+
9
+ ### Added
10
+
11
+ - `/login anthropic --manual` pairs by pasting the authorization code Anthropic shows in the browser instead of waiting on a `localhost:54545` callback the browser cannot reach. Complete it with `/login <code>`. Use it when gjc runs over SSH, in a container, or on a headless host; the default `/login anthropic` still uses the loopback callback. The flag is resolved before the paste fallback, so it is never mistaken for an authorization code, and providers without a paste-a-code redirect reject it explicitly instead of silently falling back.
12
+
13
+ ### Fixed
14
+
15
+ - Composer Bash policy rejections now identify the active provider surface and direct Cursor Composer models to their native repository tools, enabling the agent runtime's bounded automatic recovery instead of leaving a blocked shell attempt as a terminal turn.
16
+ - The `AgentSession retry fallback > invalidates an auth-failed managed credential` test now uses a stored credential instead of a runtime-key override, matching the pin-guard behavior added in #3724 where `--api-key`/`--credential` pinned keys are never invalidated; the shared test fixture installed runtime keys as plumbing, which silently tripped the new guard and blocked the auth invalidation path.
17
+ - The Extension Control Center inspector no longer crashes when a narrow two-column layout leaves its preview pane fewer than two columns wide.
18
+ - Prompt-template positional arguments now preserve literal `$@` and `$ARGUMENTS` text instead of recursively expanding it during placeholder substitution.
19
+ - Native Windows session and GC commands now report the searched `psmux` / `pmux` / `tmux` provider set when no compatible multiplexer is available instead of leaking a literal `tmux` spawn error (#3688).
20
+ - Managed-session recovery now preserves committed mutation state and the actual Linux fallback primitive in native publish receipts, preventing unsafe retry classification after a post-link staging unlink failure (#3746).
21
+ - The system prompt now requires non-ASCII tool-input text to be written as literal UTF-8 rather than hand-spelled `\uXXXX` escapes, including JSON serialized into a string field, while leaving escapes that are intended source syntax alone. Models that hand-spell hex codepoints for CJK mis-type them, and each mis-typed escape decodes to a valid-but-wrong syllable, so Korean text in tool parameters silently arrives corrupted (anthropics/claude-code#83033).
22
+ - The `models.yml` validation error for a custom provider without a credential source now explains that `auth` selects only the auth scheme and lists the three corrective forms (`apiKeyEnv`, literal `apiKey`, or `auth: none`) instead of restating the rule that was already misread. `docs/models.md` documents the same contract in a table (#3738).
23
+ - Credential rotation no longer mutates a pinned credential, and no longer reports a rotation that did not happen. `#markFailedCredential` now applies its pin guard first and for every trigger class, consulting both the `--api-key` runtime override and the `--credential` runtime selector — previously the guard existed only on the quota path and checked only the API-key override, so a `--credential` pin could be rotated away from and the `auth` path could invalidate a pinned credential outright. Both paths now also require the re-resolved credential to actually differ before reporting a rotation: `invalidateCredentialMatching` reports that a row was matched and blocked, which is not the same as the session having moved to a different credential, and with a single-row pool it was true while nothing rotated. A terminal `forbidden` failure is now excluded from retry admission and makes no credential-state change at all. Finally, a rotation is only converted into a same-model retry when the fallback controller could actually be rewound; `restorePreviousEntryForRetry()` refuses once an entry's restore budget is spent, and ignoring that refusal left `activeIndex` on the next entry while the session still requested the previous model.
24
+ - Dead-owner notification recovery now preserves a machine-readable transition block, marker-age diagnostics, and safe force-recovery guidance without weakening ownership proofs (#3762).
25
+ - Detached SDK session hosts no longer outlive the broker that spawned them. A host whose broker died without teardown (crash, `SIGKILL`, restart without `--close-session-hosts`) previously stayed resident forever, holding its session's memory — hundreds of MB per orphan. Each host now polls the broker discovery publication and, after a bounded grace period with no live broker, disposes itself through the same graceful teardown a `SIGTERM` takes. A replacement broker resets the window, so hosts still survive ordinary broker restarts, and a transient discovery read failure is treated as ambiguity rather than proof of orphanhood.
26
+ - Syntax highlighting now recognizes special filenames such as `CMakeLists.txt`, `Dockerfile.*`, `Makefile`, and `.env.*` before generic filename extensions.
27
+
5
28
  ## [0.12.8] - 2026-08-02
6
29
  ### Added
7
30
 
8
31
  - Added the paginated public SDK query `providers.list/active` (Q29), returning deterministic, deduplicated `{ provider, connectionKind }` descriptors for locally eligible providers without exposing credentials or performing remote health probes.
32
+ - Added the opt-in Alibaba Token Plan Pro profile with `deepseek-v4-flash-0731:max` for execution and `glm-5.2:xhigh` for independent criticism, preserving the existing Balanced profile unchanged.
9
33
 
10
34
  ### Added
11
35
 
@@ -13,10 +37,14 @@
13
37
 
14
38
  ### Fixed
15
39
 
40
+ - Windows automatic tmux resolution now selects `psmux` then `pmux` by canonical command order without rejecting distinct lower-priority aliases; it probes `tmux` only when neither named provider is available (#3725).
41
+ - CI failure extraction now aggregates Bun failure and suite-error summaries across every test invocation in a job log instead of silently using only the first summary.
16
42
  - Ordinary `ask` selectors now bound long question premises and page through every premise row without skipping rows hidden by overflow indicators (#3675).
17
43
  - First-event timeout retries now require a typed, content-free failure from the current clean attempt scope, preventing prior or stale extension activity from suppressing or admitting a later request (#3553).
18
44
  - The issue-1979 Korean prose wrap test now cleans up inherited multiplexer env vars (`TMUX`, `TMUX_PANE`, etc.) so it deterministically exercises the plain-terminal render path regardless of the CI runner's terminal session (#1979).
19
45
  - The model selector's assignment menu now shows the model each role currently resolves to (`Set as EXECUTOR (Executor) — now: anthropic/claude-haiku-4-5`), distinguishing an unset default, a role that inherits the default, and a configured-but-unresolvable selector. Previously the role rows were unlabeled, so the only way to learn a role's model was to scan the whole 800+ entry model list for role badges.
46
+ - Standalone `AGENTS.md` ancestor discovery now bounds directory traversal, per-file reads, and aggregate instruction bytes while surfacing content-free omission warnings (#3722).
47
+
20
48
  ## [0.12.7] - 2026-07-31
21
49
 
22
50
  ## [0.12.6] - 2026-07-31
@@ -26,6 +54,7 @@
26
54
 
27
55
  ### Fixed
28
56
 
57
+ - Managed session publication now works on filesystems that implement no `renameat2` rename flags at all. NFS rejects both `RENAME_NOREPLACE` and `RENAME_EXCHANGE` with `EINVAL`, so publishing a migration receipt failed the whole resume with `Could not open managed session: invalid_request`. The no-replace publish now falls back to `linkat(2)`, which fails with `EEXIST` on an occupied destination and therefore carries the identical no-overwrite guarantee; the fallback is authorized only by a pre-mutation missing-primitive outcome, so a publish that may have committed is never retried under a second primitive. The staged descriptor is retained across publication and the staging link is removed only after it is released, because unlinking a still-open name on NFS silly-renames it and would leave a second link on the published inode.
29
58
  - Settings now requests a repaint after asynchronous GJC bundle and plugin views rebuild, so loaded content and mutation results appear without an extra keypress (#3643).
30
59
  - `todo_write` now rejects unsupported operation keys and treats a bare `done` or `drop` as an error instead of completing or abandoning every task (#3640).
31
60
  - Deferred `agent_end` publication again settles public session readiness before slow extension handlers finish, while retaining exact cancellation leases through queued extension delivery and draining that delivery before session shutdown.
@@ -21,6 +21,7 @@ export interface NotifyCommandArgs {
21
21
  slackChannelId?: string;
22
22
  slackAuthorizedUserId?: string;
23
23
  redact?: boolean;
24
+ forceDaemonLock?: boolean;
24
25
  probe?: boolean;
25
26
  message?: string;
26
27
  }
@@ -3,6 +3,29 @@ import type { Args as ParsedArgs } from "../cli/args";
3
3
  import { type SessionLifecycleLaunchRequest } from "../sdk/broker/lifecycle";
4
4
  import { SessionManager } from "../session/session-manager";
5
5
  export declare function lifecycleArgs(request: SessionLifecycleLaunchRequest, cwd: string, agentDir: string): Promise<ParsedArgs>;
6
+ /**
7
+ * How long a session host tolerates the complete absence of a live broker
8
+ * publication before treating itself as orphaned. Hosts intentionally survive
9
+ * broker restarts (a replacement broker republishes discovery within seconds),
10
+ * so this must comfortably exceed a restart window while still bounding the
11
+ * lifetime of hosts whose broker is gone for good — otherwise every crashed or
12
+ * torn-down broker leaks a detached multi-hundred-megabyte host forever.
13
+ */
14
+ export declare const SESSION_HOST_BROKER_ABSENCE_GRACE_MS: number;
15
+ /**
16
+ * Resolves only once no live broker publication has been observable in
17
+ * `agentDir` for the full grace window. A reappearing broker (including a
18
+ * replacement with a different pid) resets the window; an unreadable
19
+ * publication is not proof of orphanhood but accrues against the same bound.
20
+ */
21
+ export declare function watchSessionHostBrokerLiveness(deps: {
22
+ agentDir: string;
23
+ now?: () => number;
24
+ sleep?: (ms: number) => Promise<void>;
25
+ readDiscovery?: (agentDir: string) => Promise<unknown>;
26
+ graceMs?: number;
27
+ pollMs?: number;
28
+ }): Promise<void>;
6
29
  /** Opens lifecycle-authorized history without letting replacement content reach readiness. */
7
30
  export declare function openLifecycleSessionManager(request: SessionLifecycleLaunchRequest, cwd: string, agentDir: string): Promise<{
8
31
  parsed: ParsedArgs;
@@ -1 +1,11 @@
1
- export {};
1
+ import { type ContextFile } from "../capability/context-file";
2
+ import type { LoadContext, LoadResult } from "../capability/types";
3
+ export type AgentsMdReader = (filePath: string, maxBytes: number) => Promise<{
4
+ content: string | null;
5
+ byteLength: number;
6
+ tooLarge: boolean;
7
+ }>;
8
+ /**
9
+ * Load standalone AGENTS.md files.
10
+ */
11
+ export declare function loadAgentsMd(ctx: LoadContext, readCandidate?: AgentsMdReader): Promise<LoadResult<ContextFile>>;
@@ -374,5 +374,11 @@ export interface OAuthSelectorOptions {
374
374
  allowExternalCredentialDiscovery?: boolean;
375
375
  trigger?: "bare-login";
376
376
  externalCredentialDiscover?: CredentialAutoImportOptions["discover"];
377
+ /**
378
+ * Pair by pasting the code the provider displays instead of waiting on the
379
+ * loopback callback. Set by `/login <provider> --manual` for browsers that
380
+ * cannot reach this machine.
381
+ */
382
+ manualCode?: boolean;
377
383
  }
378
384
  export {};
@@ -13,6 +13,7 @@ export interface DaemonPaths {
13
13
  ownership: string;
14
14
  roots: string;
15
15
  steal: string;
16
+ diagnostic: string;
16
17
  aliases: string;
17
18
  seenUpdates: string;
18
19
  }
@@ -9,6 +9,21 @@ import type { SlackDiagnosticProvider } from "./slack-provider";
9
9
  * can never leak a credential regardless of where the string originated.
10
10
  */
11
11
  export declare function sanitizeDiagnostic(text: string, token?: string): string;
12
+ export interface NotificationDiagnosticEvent {
13
+ timestamp: string;
14
+ operation: string;
15
+ phase: string;
16
+ outcome: string;
17
+ reason: string;
18
+ pid?: number;
19
+ incarnation?: string;
20
+ ageMs?: number;
21
+ detail?: string;
22
+ }
23
+ /** Append a bounded, private, secret-safe daemon diagnostic event. */
24
+ export declare function writeNotificationDiagnostic(settings: Pick<Settings, "getAgentDir">, event: Omit<NotificationDiagnosticEvent, "timestamp"> & {
25
+ timestamp?: string;
26
+ }): Promise<void>;
12
27
  /** Identity evidence required to remove precisely the endpoint that was inspected. */
13
28
  export interface NotificationEndpointFileIdentity {
14
29
  dev: bigint;
@@ -54,6 +69,7 @@ export interface NotificationServiceDeps {
54
69
  fs?: NotificationServiceFs;
55
70
  now?: () => number;
56
71
  pidAlive?: (pid: number) => boolean;
72
+ pidIncarnation?: (pid: number) => string | undefined;
57
73
  fetchImpl?: typeof fetch;
58
74
  apiBase?: string;
59
75
  createDiscordDiagnostic?: (config: {
@@ -223,6 +239,9 @@ export interface NotificationRecoveryReport {
223
239
  action: DaemonRecoveryAction;
224
240
  detail: string;
225
241
  ownerId: string | undefined;
242
+ blockingReason?: string;
243
+ markerAgeMs?: number;
244
+ forceCommand?: string;
226
245
  pid: number | undefined;
227
246
  };
228
247
  }
@@ -230,6 +249,7 @@ export interface RecoveryOptions {
230
249
  settings: Settings;
231
250
  stateRoot?: string;
232
251
  deps?: NotificationServiceDeps;
252
+ forceDaemonLock?: boolean;
233
253
  }
234
254
  export interface DaemonTransitionLock {
235
255
  pid: number;
@@ -243,6 +263,9 @@ type TransitionMarkerFs = {
243
263
  writeFile?(file: string, data: string, opts?: WriteFileOptions): Promise<void>;
244
264
  readEndpointFile?(file: string): Promise<NotificationEndpointFile>;
245
265
  exactUnlink?(file: string, identity: NotificationEndpointFileIdentity): Promise<NotificationExactUnlinkResult>;
266
+ stat?(file: string): Promise<{
267
+ mtimeMs: number;
268
+ }>;
246
269
  };
247
270
  /** True only while the exact transition acquisition still occupies the marker path. */
248
271
  export declare function daemonTransitionLockIsHeld(input: {
@@ -53,10 +53,10 @@ export declare const NOTIFICATION_PROTOCOL_VERSION = 3;
53
53
  * ownership, settlement, and descendant cleanup authority. Generation 38 also
54
54
  * adds durable provider-intent admission without changing owner, reclaim,
55
55
  * signal, or spawn authority.
56
- * Generation 41 applies first-class provider-settings admission to Telegram
56
+ * Generation 42 applies first-class provider-settings admission to Telegram
57
57
  * lifecycle controls.
58
58
  */
59
- export declare const DAEMON_GENERATION = 41;
59
+ export declare const DAEMON_GENERATION = 42;
60
60
  /**
61
61
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
62
62
  * all builds published before this field existed; epoch 2 covered generation 29;
@@ -12,6 +12,14 @@ export declare class ManagedPublishError extends Error {
12
12
  }
13
13
  /** Cleanup is safe only when the native move reports that it did not commit. */
14
14
  export declare function mayCleanManagedTreeStaging(error: unknown): boolean;
15
+ /**
16
+ * A filesystem that implements no `renameat2` rename flag at all rejects the no-replace publish
17
+ * before mutating anything: NFS answers `EINVAL` (`invalid_request`) and kernels older than 3.15
18
+ * answer `ENOSYS` (`atomic_unavailable`). Only those two pre-mutation outcomes authorize the
19
+ * `linkat` stand-in; every other failure describes a publish that was actually attempted, and
20
+ * retrying it under a different primitive could publish twice.
21
+ */
22
+ export declare function renameFlagsUnsupported(outcome: NativePublishOutcome): boolean;
15
23
  export type ManagedSessionSecurityPolicy = "default" | "windows-existing-verify-first";
16
24
  export type ManagedStorageFailure = "migration_busy" | "binding_conflict" | "binding_invalid" | "destination_conflict" | "source_changed" | "unsafe_artifacts" | "artifact_capacity_exceeded" | "durability_failed" | "atomic_unavailable" | "durability_not_provable" | "migration_retired" | "managed_storage_unsupported";
17
25
  export interface ManagedStorageLock {
@@ -2,8 +2,8 @@ export type NativePublishMutationState = "not_committed" | "committed" | "unknow
2
2
  export type NativePublishDurabilityState = "not_attempted" | "proven" | "not_provable";
3
3
  export type NativePublishReason = "none" | "destination_exists" | "atomic_unavailable" | "cross_device" | "permission_denied" | "io_failure" | "interrupted" | "invalid_request" | "identity_violation" | "durability_not_provable" | "unknown";
4
4
  export type NativePublishOperation = "direct_rename" | "retained_file" | "retained_tree";
5
- export type NativePublishPrimitive = "renameat2_noreplace" | "renameatx_np_excl" | "windows_rename_noreplace" | "unsupported" | "unknown";
6
- export type NativePublishPhase = "preflight" | "file_sync" | "rename" | "source_parent_sync" | "destination_parent_sync" | "terminal_identity" | "complete" | "unknown";
5
+ export type NativePublishPrimitive = "renameat2_noreplace" | "linkat_noreplace" | "mkdirat_renameat_noreplace" | "renameatx_np_excl" | "windows_rename_noreplace" | "unsupported" | "unknown";
6
+ export type NativePublishPhase = "preflight" | "file_sync" | "rename" | "source_unlink" | "source_parent_sync" | "destination_parent_sync" | "terminal_identity" | "complete" | "unknown";
7
7
  type SyncFailure = {
8
8
  phase: Exclude<NativePublishPhase, "preflight" | "file_sync" | "rename" | "complete" | "unknown">;
9
9
  parentRole: "source" | "destination" | "shared" | "staged_file";
@@ -16,6 +16,14 @@ export interface LoadContextFilesOptions {
16
16
  /** Working directory to start walking up from. Default: getProjectDir() */
17
17
  cwd?: string;
18
18
  }
19
+ export interface ProjectContextFilesResult {
20
+ contextFiles: Array<{
21
+ path: string;
22
+ content: string;
23
+ depth?: number;
24
+ }>;
25
+ warnings: string[];
26
+ }
19
27
  /**
20
28
  * Load all context files using the capability API.
21
29
  * Returns {path, content, depth} entries for all discovered context files.
@@ -24,6 +32,8 @@ export interface LoadContextFilesOptions {
24
32
  * prominent. User-home files from foreign providers (`~/.claude/CLAUDE.md`,
25
33
  * `~/.codex/AGENTS.md`, …) stay excluded — only gjc's own user config applies.
26
34
  */
35
+ export declare function loadProjectContextFilesResult(options?: LoadContextFilesOptions): Promise<ProjectContextFilesResult>;
36
+ /** Load project context files without exposing discovery diagnostics. */
27
37
  export declare function loadProjectContextFiles(options?: LoadContextFilesOptions): Promise<Array<{
28
38
  path: string;
29
39
  content: string;
@@ -93,10 +103,11 @@ export interface BuildSystemPromptOptions {
93
103
  */
94
104
  subagent?: boolean;
95
105
  }
96
- /** Result of building provider-facing system prompt messages. */
97
106
  export interface BuildSystemPromptResult {
98
107
  /** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
99
108
  systemPrompt: string[];
109
+ /** Context-file discovery warnings visible to SDK and session callers. */
110
+ warnings: string[];
100
111
  }
101
112
  export interface BuildVolatileProjectContextOptions {
102
113
  cwd?: string;
@@ -1,14 +1,22 @@
1
- export declare const COMPOSER_BASH_POLICY_ERROR = "Composer bash policy blocked repository file I/O. Use find, search, read, and edit tools for file discovery, file inspection, and file mutation.";
1
+ import { type ComposerBashPolicyToolSurface } from "@gajae-code/ai/providers/composer-discipline";
2
+ export declare const COMPOSER_BASH_POLICY_ERROR: string;
3
+ export declare const CURSOR_COMPOSER_BASH_POLICY_ERROR: string;
2
4
  type ComposerBashPolicyResult = {
3
5
  allowed: true;
4
6
  } | {
5
7
  allowed: false;
6
8
  reason: string;
7
9
  message: string;
10
+ surface: ComposerBashPolicyToolSurface;
8
11
  };
9
12
  export declare function isComposerBashPolicyModel(modelId: string | undefined): boolean;
13
+ export declare function resolveComposerBashPolicyToolSurface(input: {
14
+ modelId?: string;
15
+ provider?: string;
16
+ }): ComposerBashPolicyToolSurface;
10
17
  export declare function checkComposerBashPolicy(input: {
11
18
  modelId?: string;
19
+ provider?: string;
12
20
  commands: readonly string[];
13
21
  }): ComposerBashPolicyResult;
14
22
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.12.8",
4
+ "version": "0.12.10",
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",
@@ -53,13 +53,13 @@
53
53
  "@agentclientprotocol/sdk": "1.3.0",
54
54
  "@babel/parser": "^7.29.3",
55
55
  "@mozilla/readability": "^0.6.0",
56
- "@gajae-code/stats": "0.12.8",
57
- "@gajae-code/agent-core": "0.12.8",
58
- "@gajae-code/ai": "0.12.8",
59
- "@gajae-code/bridge-client": "0.12.8",
60
- "@gajae-code/natives": "0.12.8",
61
- "@gajae-code/tui": "0.12.8",
62
- "@gajae-code/utils": "0.12.8",
56
+ "@gajae-code/stats": "0.12.10",
57
+ "@gajae-code/agent-core": "0.12.10",
58
+ "@gajae-code/ai": "0.12.10",
59
+ "@gajae-code/bridge-client": "0.12.10",
60
+ "@gajae-code/natives": "0.12.10",
61
+ "@gajae-code/tui": "0.12.10",
62
+ "@gajae-code/utils": "0.12.10",
63
63
  "@puppeteer/browsers": "^2.13.0",
64
64
  "@types/turndown": "5.0.6",
65
65
  "@xterm/headless": "^6.0.0",
@@ -1531,13 +1531,26 @@ function exactTeamRuntimeSendKeysRanges(contents: string): ShellRange[] {
1531
1531
  return [];
1532
1532
 
1533
1533
  const continuationBody = contents.slice(continuationRange.start, continuationRange.end);
1534
+ // The frozen argv may address the pane either through `worker.pane_id` directly or
1535
+ // through a local binding that was proven non-empty first (the optional field does
1536
+ // not narrow for the type checker). Either way both send operations must name the
1537
+ // identical pane token, and a local token must come from a checked `worker.pane_id`.
1534
1538
  const continuationArgs =
1535
- /const\s+args\s*=\s*Object\.freeze\(\s*\[\s*["']send-keys["']\s*,\s*["']-l["']\s*,\s*["']-t["']\s*,\s*worker\.pane_id\s*,\s*continuationPrompt\s*,\s*["'];["']\s*,\s*["']send-keys["']\s*,\s*["']-t["']\s*,\s*worker\.pane_id\s*,\s*["']Enter["']\s*,?\s*\]\s*\)\s*;/.exec(
1539
+ /const\s+args(?:\s*:\s*readonly\s+string\[\])?\s*=\s*Object\.freeze\(\s*\[\s*["']send-keys["']\s*,\s*["']-l["']\s*,\s*["']-t["']\s*,\s*(worker\.pane_id|[A-Za-z_$][\w$]*)\s*,\s*continuationPrompt\s*,\s*["'];["']\s*,\s*["']send-keys["']\s*,\s*["']-t["']\s*,\s*(worker\.pane_id|[A-Za-z_$][\w$]*)\s*,\s*["']Enter["']\s*,?\s*\]\s*\)\s*;/.exec(
1536
1540
  continuationBody,
1537
1541
  );
1542
+ const paneToken = continuationArgs?.[1];
1543
+ const paneTokenIsProvenLocal =
1544
+ paneToken !== undefined &&
1545
+ paneToken !== "worker.pane_id" &&
1546
+ new RegExp(`const\\s+${paneToken}\\s*=\\s*worker\\.pane_id\\s*;`).test(continuationBody) &&
1547
+ new RegExp(`if\\s*\\(\\s*!${paneToken}\\s*\\)\\s*return\\b`).test(continuationBody);
1538
1548
  if (
1539
1549
  !continuationArgs ||
1550
+ continuationArgs[1] !== continuationArgs[2] ||
1551
+ !(paneToken === "worker.pane_id" || paneTokenIsProvenLocal) ||
1540
1552
  !continuationBody.includes("const revalidationReason =") ||
1553
+ !/if\s*\(\s*revalidationReason\s*\)\s*(?:\{\s*return\b[^}]*;?\s*\}|return\b[^;]*;)/.test(continuationBody) ||
1541
1554
  !continuationBody.includes("await createJsonNoClobber(") ||
1542
1555
  !continuationBody.includes('type: "literal-send"') ||
1543
1556
  !continuationBody.includes('type: "key-send"') ||
@@ -69,6 +69,7 @@ export interface NotifyCommandArgs {
69
69
  slackChannelId?: string;
70
70
  slackAuthorizedUserId?: string;
71
71
  redact?: boolean;
72
+ forceDaemonLock?: boolean;
72
73
  probe?: boolean;
73
74
  message?: string;
74
75
  }
@@ -193,7 +194,12 @@ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
193
194
  message: typeof flags.get("--message") === "string" ? (flags.get("--message") as string) : undefined,
194
195
  };
195
196
  }
196
- if (action === "recovery") return args.length === 2 ? { action, rawArgs: [] } : undefined;
197
+ if (action === "recovery") {
198
+ const flags = parseFlags(args.slice(2), new Set(), new Set(["--force-daemon-lock"]));
199
+ return flags
200
+ ? { action, rawArgs: args.slice(2), forceDaemonLock: flags.get("--force-daemon-lock") === true }
201
+ : undefined;
202
+ }
197
203
  if (action === "daemon-internal") {
198
204
  return {
199
205
  action,
@@ -224,7 +230,7 @@ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyComma
224
230
  await runTest(deps, cmd);
225
231
  return;
226
232
  case "recovery":
227
- await runRecovery(deps);
233
+ await runRecovery(deps, cmd.forceDaemonLock);
228
234
  return;
229
235
  case "daemon-internal":
230
236
  if (cmd.smoke) {
@@ -716,9 +722,9 @@ async function runTest(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promise
716
722
  else if (!result.ok) process.exitCode = 1;
717
723
  }
718
724
 
719
- async function runRecovery(deps: NotifyCommandDeps): Promise<void> {
725
+ async function runRecovery(deps: NotifyCommandDeps, forceDaemonLock = false): Promise<void> {
720
726
  const settings = await getSettings(deps);
721
- const report = await recoverNotifications({ settings });
727
+ const report = await recoverNotifications({ settings, forceDaemonLock });
722
728
  process.stdout.write(`${formatNotificationRecoveryReport(report)}\n`);
723
729
  }
724
730
 
@@ -737,14 +743,14 @@ ${chalk.bold("Usage:")}
737
743
  ${APP_NAME} notify status
738
744
  ${APP_NAME} notify health [--provider telegram|discord|slack] [--probe]
739
745
  ${APP_NAME} notify test [--provider telegram|discord|slack] [--message <text>]
740
- ${APP_NAME} notify recovery
746
+ ${APP_NAME} notify recovery [--force-daemon-lock]
741
747
 
742
748
  ${chalk.bold("Subcommands:")}
743
749
  setup Pair Telegram or atomically save and activate complete Discord/Slack settings
744
750
  status Show global master and provider configured/repair/desired/effective state without secrets
745
751
  health Report selected provider state; --probe uses REST only and never opens Gateway/Socket Mode
746
752
  test Send a one-off test through one selected or uniquely effective provider
747
- recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner)
753
+ recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner); --force-daemon-lock retries only with the same fail-closed dead-owner proof
748
754
 
749
755
  ${chalk.bold("Examples:")}
750
756
  ${APP_NAME} notify setup
@@ -9,6 +9,7 @@ import { applyStartupModelProfiles, createSessionManager } from "../main";
9
9
  import { initializeExtensions } from "../modes/runtime-init";
10
10
  import { ACP_MCP_REQUEST_TIMEOUT_MS, ACP_MCP_STARTUP_HEADROOM_MS } from "../sdk/acp/mcp";
11
11
  import { Broker } from "../sdk/broker/broker";
12
+ import { readBrokerDiscovery } from "../sdk/broker/discovery";
12
13
  import { completeBrokerProcess } from "../sdk/broker/internal";
13
14
  import {
14
15
  type LifecycleTranscriptEvidence,
@@ -58,6 +59,54 @@ export async function lifecycleArgs(
58
59
  };
59
60
  }
60
61
 
62
+ /**
63
+ * How long a session host tolerates the complete absence of a live broker
64
+ * publication before treating itself as orphaned. Hosts intentionally survive
65
+ * broker restarts (a replacement broker republishes discovery within seconds),
66
+ * so this must comfortably exceed a restart window while still bounding the
67
+ * lifetime of hosts whose broker is gone for good — otherwise every crashed or
68
+ * torn-down broker leaks a detached multi-hundred-megabyte host forever.
69
+ */
70
+ export const SESSION_HOST_BROKER_ABSENCE_GRACE_MS = 10 * 60_000;
71
+ const SESSION_HOST_BROKER_POLL_MS = 15_000;
72
+
73
+ /**
74
+ * Resolves only once no live broker publication has been observable in
75
+ * `agentDir` for the full grace window. A reappearing broker (including a
76
+ * replacement with a different pid) resets the window; an unreadable
77
+ * publication is not proof of orphanhood but accrues against the same bound.
78
+ */
79
+ export async function watchSessionHostBrokerLiveness(deps: {
80
+ agentDir: string;
81
+ now?: () => number;
82
+ sleep?: (ms: number) => Promise<void>;
83
+ readDiscovery?: (agentDir: string) => Promise<unknown>;
84
+ graceMs?: number;
85
+ pollMs?: number;
86
+ }): Promise<void> {
87
+ const now = deps.now ?? Date.now;
88
+ const sleep = deps.sleep ?? (async ms => await Bun.sleep(ms));
89
+ const readDiscovery = deps.readDiscovery ?? readBrokerDiscovery;
90
+ const graceMs = deps.graceMs ?? SESSION_HOST_BROKER_ABSENCE_GRACE_MS;
91
+ const pollMs = deps.pollMs ?? SESSION_HOST_BROKER_POLL_MS;
92
+ let absentSince: number | null = null;
93
+ for (;;) {
94
+ let live: unknown = null;
95
+ try {
96
+ live = await readDiscovery(deps.agentDir);
97
+ } catch {
98
+ // Transient read failures are ambiguity, not proof of orphanhood.
99
+ }
100
+ if (live) {
101
+ absentSince = null;
102
+ } else {
103
+ absentSince ??= now();
104
+ if (now() - absentSince >= graceMs) return;
105
+ }
106
+ await sleep(pollMs);
107
+ }
108
+ }
109
+
61
110
  type LifecycleTranscriptSource = {
62
111
  cwd: string;
63
112
  path: string;
@@ -505,6 +554,11 @@ export async function runSessionHost(
505
554
  }
506
555
  process.once("SIGTERM", stop);
507
556
  process.once("SIGINT", stop);
557
+ // A detached host whose broker is gone for good would otherwise live (and
558
+ // hold its session's memory) forever; reap it through the same graceful
559
+ // teardown a SIGTERM would take once the bounded absence grace elapses.
560
+ await watchSessionHostBrokerLiveness({ agentDir });
561
+ stop();
508
562
  await new Promise<void>(() => {});
509
563
  }
510
564
 
@@ -291,6 +291,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
291
291
  architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
292
292
  critic: "alibaba-token-plan/glm-5.2:high",
293
293
  }),
294
+ profile("alibaba-token-plan-pro", ["alibaba-token-plan"], {
295
+ default: "alibaba-token-plan/qwen3.8-max-preview:medium",
296
+ executor: "alibaba-token-plan/deepseek-v4-flash-0731:max",
297
+ planner: "alibaba-token-plan/glm-5.2:high",
298
+ architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
299
+ critic: "alibaba-token-plan/glm-5.2:xhigh",
300
+ }),
294
301
  profile("alibaba-token-plan-qwenmaxxing", ["alibaba-token-plan"], {
295
302
  default: "alibaba-token-plan/qwen3.8-max-preview:medium",
296
303
  executor: "alibaba-token-plan/qwen3.8-max-preview:low",
@@ -361,6 +368,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
361
368
  "minimax-medium": { displayName: "MiniMax Medium", providerGroup: "MINIMAX" },
362
369
  "minimax-pro": { displayName: "MiniMax Pro", providerGroup: "MINIMAX" },
363
370
  "alibaba-token-plan-balanced": { displayName: "Balanced", providerGroup: "ALIBABA TOKEN PLAN" },
371
+ "alibaba-token-plan-pro": { displayName: "Pro", providerGroup: "ALIBABA TOKEN PLAN" },
364
372
  "alibaba-token-plan-qwenmaxxing": { displayName: "QwenMaxxing", providerGroup: "ALIBABA TOKEN PLAN" },
365
373
  "opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
366
374
  "codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
@@ -331,7 +331,10 @@ function validateProviderConfiguration(
331
331
  throw new Error(
332
332
  mode === "runtime-register"
333
333
  ? `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`
334
- : `Provider ${providerName}: "apiKey" or "apiKeyEnv" is required when defining custom models unless auth is "none".`,
334
+ : `Provider ${providerName}: custom models need a credential source, but none is configured. ` +
335
+ `"auth" only selects the scheme ("auth: apiKey" does not supply a key). ` +
336
+ `Fix by adding "apiKeyEnv: <ENV_VAR>" (recommended) or "apiKey: <literal-key>", ` +
337
+ `or set "auth: none" if the endpoint is genuinely unauthenticated.`,
335
338
  );
336
339
  }
337
340
  }