@gajae-code/coding-agent 0.7.1 → 0.7.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 (92) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/cli/notify-cli.d.ts +2 -0
  3. package/dist/types/config/settings-schema.d.ts +39 -2
  4. package/dist/types/extensibility/shared-events.d.ts +1 -0
  5. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +1 -1
  6. package/dist/types/lsp/types.d.ts +2 -0
  7. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  8. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  9. package/dist/types/notifications/config.d.ts +9 -1
  10. package/dist/types/notifications/engine.d.ts +59 -0
  11. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  12. package/dist/types/notifications/telegram-daemon.d.ts +19 -0
  13. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  14. package/dist/types/notifications/threaded-render.d.ts +6 -1
  15. package/dist/types/session/agent-session.d.ts +2 -0
  16. package/dist/types/tools/fetch.d.ts +23 -0
  17. package/dist/types/tools/index.d.ts +1 -0
  18. package/dist/types/tools/telegram-send.d.ts +32 -0
  19. package/dist/types/web/insane/bridge.d.ts +103 -0
  20. package/dist/types/web/insane/url-guard.d.ts +22 -0
  21. package/dist/types/web/search/provider.d.ts +18 -1
  22. package/dist/types/web/search/providers/insane.d.ts +53 -0
  23. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  24. package/dist/types/web/search/types.d.ts +12 -4
  25. package/package.json +10 -8
  26. package/scripts/verify-insane-vendor.ts +132 -0
  27. package/src/cli/args.ts +1 -1
  28. package/src/cli/fast-help.ts +1 -1
  29. package/src/cli/notify-cli.ts +152 -5
  30. package/src/commands/team.ts +1 -1
  31. package/src/config/settings-schema.ts +30 -1
  32. package/src/defaults/gjc/skills/ralplan/SKILL.md +11 -4
  33. package/src/extensibility/shared-events.ts +1 -0
  34. package/src/gjc-runtime/launch-tmux.ts +17 -3
  35. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  36. package/src/gjc-runtime/ralplan-runtime.ts +2 -2
  37. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  38. package/src/gjc-runtime/workflow-manifest.ts +7 -2
  39. package/src/internal-urls/docs-index.generated.ts +7 -7
  40. package/src/lsp/config.ts +16 -3
  41. package/src/lsp/defaults.json +7 -0
  42. package/src/lsp/types.ts +2 -0
  43. package/src/modes/controllers/event-controller.ts +15 -0
  44. package/src/modes/interactive-mode.ts +46 -2
  45. package/src/modes/utils/context-usage.ts +2 -2
  46. package/src/notifications/attachment-registry.ts +23 -0
  47. package/src/notifications/chat-adapters.ts +147 -0
  48. package/src/notifications/config.ts +23 -2
  49. package/src/notifications/engine.ts +100 -0
  50. package/src/notifications/index.ts +180 -38
  51. package/src/notifications/managed-daemon.ts +163 -0
  52. package/src/notifications/telegram-daemon.ts +235 -14
  53. package/src/notifications/threaded-inbound.ts +60 -4
  54. package/src/notifications/threaded-render.ts +20 -2
  55. package/src/session/agent-session.ts +82 -51
  56. package/src/tools/fetch.ts +78 -1
  57. package/src/tools/index.ts +3 -0
  58. package/src/tools/telegram-send.ts +137 -0
  59. package/src/web/insane/bridge.ts +350 -0
  60. package/src/web/insane/url-guard.ts +155 -0
  61. package/src/web/search/provider.ts +77 -18
  62. package/src/web/search/providers/anthropic.ts +70 -3
  63. package/src/web/search/providers/codex.ts +1 -119
  64. package/src/web/search/providers/gemini.ts +99 -0
  65. package/src/web/search/providers/insane.ts +551 -0
  66. package/src/web/search/providers/openai-compatible.ts +66 -32
  67. package/src/web/search/providers/text-citations.ts +111 -0
  68. package/src/web/search/types.ts +13 -2
  69. package/vendor/insane-search/LICENSE +21 -0
  70. package/vendor/insane-search/MANIFEST.json +24 -0
  71. package/vendor/insane-search/engine/__init__.py +23 -0
  72. package/vendor/insane-search/engine/__main__.py +128 -0
  73. package/vendor/insane-search/engine/bias_check.py +183 -0
  74. package/vendor/insane-search/engine/executor.py +254 -0
  75. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  76. package/vendor/insane-search/engine/learning.py +175 -0
  77. package/vendor/insane-search/engine/phase0.py +214 -0
  78. package/vendor/insane-search/engine/safety.py +91 -0
  79. package/vendor/insane-search/engine/templates/package.json +11 -0
  80. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  81. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  82. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  83. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  84. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  85. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  86. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  87. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  88. package/vendor/insane-search/engine/transport.py +211 -0
  89. package/vendor/insane-search/engine/url_transforms.py +98 -0
  90. package/vendor/insane-search/engine/validators.py +331 -0
  91. package/vendor/insane-search/engine/waf_detector.py +214 -0
  92. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.2] - 2026-06-24
6
+ ### Added
7
+
8
+ - Added a keyless `insane` web search provider that safely ports upstream insane-search public-route fallbacks without TLS impersonation, browser/cookie bypasses, credential storage, or auto-installed dependencies (#1011).
9
+ - `web_search` `auto` mode now drives native provider search over proxies/custom endpoints by reusing the active model's own credential + baseUrl when canonical native creds are absent: `activeContextNativeId()` matches the model's wire api (+ model-id family) to `anthropic` (anthropic-messages), `openai-compatible` (openai-responses/completions), or `gemini` (google-generative-ai Generative Language), each falling back to DuckDuckGo if the endpoint does not support web search.
10
+ - Added built-in C# LSP detection for `csharp-ls`, with `omnisharp` preserved as a fallback when `csharp-ls` is unavailable (#1054).
11
+ - Added Discord and Slack notification adapters alongside the existing Telegram surface, so action-needed signals and replies can be routed to those clients (#1043).
12
+ - Telegram daemon now supports inbound and outbound photo/file attachments, forwarding agent images and accepting user-sent media (#1053).
13
+ - `gjc` verifies Telegram Threaded Mode during notification setup and falls back to a flat private chat when topics are unavailable (#1029).
14
+
15
+ ### Fixed
16
+
17
+ - Hardened context-overflow recovery so automatic maintenance clears the TUI loader, surfaces overflow completion/skip status, retries resumable tails safely, and falls back to the synthetic auto-continue prompt for non-resumable tails when enabled.
18
+ - `web_search` native providers no longer discard genuinely grounded answers that omit structured `url_citation` annotations: when a search demonstrably ran — Responses `web_search_call` / `tool_usage.web_search`, a Chat Completions search request, or Anthropic `web_search_tool_result` / `server_tool_use` / `server_tool_use.web_search_requests` — sources are recovered from inline markdown links and bare URLs. Inline recovery is gated on that real-search signal so a stray prose URL in a non-search answer is never promoted to a citation, and Anthropic now fails closed to DuckDuckGo when Claude answers from stable knowledge without searching. Inline-citation helpers are shared via `providers/text-citations.ts`.
19
+ - Preserve GJC-managed tmux sessions on attach/disconnect instead of tearing them down, and stop implicitly attaching on launch (#1063).
20
+ - Corrected the auto-compaction output reserve so post-compaction responses keep adequate headroom (#1021).
21
+ - Improved active-input shortcut hints and the busy-input queueing hint for clearer in-session guidance (#1022, #1024).
22
+
5
23
  ## [0.7.1] - 2026-06-23
6
24
  ### Fixed
7
25
 
@@ -136,6 +154,7 @@
136
154
  - Added an opt-in `gjc rlm` research mode (v1, interactive): a Jupyter-notebook-style research session over the existing agent loop, backed by the shared persistent Python kernel. It loads a distinct research system prompt, restricts the toolset to a hard-gated allowlist (`python` + `read` + `web_search`, asserted after tool-registry assembly — no `bash`/edit/arbitrary mutation), optionally loads a project-root `DATA.md` (overridable via `--data <path>`), aggregates every executed cell live into `.gjc/rlm/<session>/notebook.ipynb` (single-queue atomic temp-rename writes with post-write validation), and synthesizes `.gjc/rlm/<session>/report.md` on session exit. Autonomous goal-arg runs, `--resume`, managed per-workspace venv provisioning, and the optional `>=N` completion gate are deferred follow-ups.
137
155
  - Added an experimental opt-in `computer` desktop-control tool surface for local macOS screenshot/input coordination, backed by native `ComputerController`/`computerScreenshot` bindings and gated through settings/tool registration so it can continue stabilizing on `dev` outside the 0.5.4 patch release.
138
156
  - Dropped deprecated GitHub Actions Intel macOS (`macos-13` / `darwin-x64`) release-binary coverage after the runner pool repeatedly blocked v0.6.0 publish; Intel macOS users should install through npm/Bun or build from source.
157
+ - Re-enabled GitHub Actions Intel macOS (`darwin-x64`) release-binary coverage using the `macos-15-intel` runner, so standalone `gjc-darwin-x64` binaries ship again alongside Apple Silicon.
139
158
 
140
159
  ## [0.5.4] - 2026-06-17
141
160
 
@@ -17,6 +17,8 @@ export interface NotifyCommandDeps {
17
17
  pollIntervalMs?: number;
18
18
  setupChatId?: string;
19
19
  setupRedact?: boolean;
20
+ setupInteractive?: boolean;
21
+ threadedModePrompt?: (message: string) => Promise<string>;
20
22
  }
21
23
  export declare function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined;
22
24
  export declare function runNotifyCommand(cmd: NotifyCommandArgs, deps?: NotifyCommandDeps): Promise<void>;
@@ -130,6 +130,22 @@ export declare const SETTINGS_SCHEMA: {
130
130
  readonly type: "string";
131
131
  readonly default: undefined;
132
132
  };
133
+ readonly "notifications.discord.botToken": {
134
+ readonly type: "string";
135
+ readonly default: undefined;
136
+ };
137
+ readonly "notifications.discord.channelId": {
138
+ readonly type: "string";
139
+ readonly default: undefined;
140
+ };
141
+ readonly "notifications.slack.botToken": {
142
+ readonly type: "string";
143
+ readonly default: undefined;
144
+ };
145
+ readonly "notifications.slack.channelId": {
146
+ readonly type: "string";
147
+ readonly default: undefined;
148
+ };
133
149
  readonly "notifications.redact": {
134
150
  readonly type: "boolean";
135
151
  readonly default: false;
@@ -2469,6 +2485,15 @@ export declare const SETTINGS_SCHEMA: {
2469
2485
  readonly description: "Allow the read tool to fetch and process URLs";
2470
2486
  };
2471
2487
  };
2488
+ readonly "web.insaneFallback": {
2489
+ readonly type: "boolean";
2490
+ readonly default: false;
2491
+ readonly ui: {
2492
+ readonly tab: "tools";
2493
+ readonly label: "Insane Search Fallback";
2494
+ readonly description: "Opt in to the vendored insane-search escalation for blocked public URL reads (403/WAF/JS-gated). Off by default. Requires preinstalled python3 + curl_cffi (and node + playwright/stealth for the browser phase); changes network posture by enabling TLS/browser impersonation for public pages.";
2495
+ };
2496
+ };
2472
2497
  readonly "github.enabled": {
2473
2498
  readonly type: "boolean";
2474
2499
  readonly default: false;
@@ -2518,7 +2543,7 @@ export declare const SETTINGS_SCHEMA: {
2518
2543
  readonly type: "array";
2519
2544
  readonly default: string[];
2520
2545
  readonly items: {
2521
- readonly enum: readonly ["duckduckgo", "exa", "brave", "jina", "kimi", "zai", "anthropic", "perplexity", "gemini", "codex", "xai", "tavily", "parallel", "kagi", "synthetic", "searxng"];
2546
+ readonly enum: readonly ["duckduckgo", "insane", "exa", "brave", "jina", "kimi", "zai", "anthropic", "perplexity", "gemini", "codex", "xai", "tavily", "parallel", "kagi", "synthetic", "searxng"];
2522
2547
  };
2523
2548
  readonly ui: {
2524
2549
  readonly tab: "tools";
@@ -3121,7 +3146,7 @@ export declare const SETTINGS_SCHEMA: {
3121
3146
  };
3122
3147
  readonly "providers.webSearch": {
3123
3148
  readonly type: "enum";
3124
- readonly values: readonly ["auto", "duckduckgo", "exa", "brave", "jina", "kimi", "zai", "perplexity", "anthropic", "gemini", "codex", "xai", "tavily", "kagi", "synthetic", "parallel", "searxng"];
3149
+ readonly values: readonly ["auto", "duckduckgo", "insane", "exa", "brave", "jina", "kimi", "zai", "perplexity", "anthropic", "gemini", "codex", "xai", "tavily", "kagi", "synthetic", "parallel", "searxng"];
3125
3150
  readonly default: "auto";
3126
3151
  readonly ui: {
3127
3152
  readonly tab: "providers";
@@ -3135,6 +3160,10 @@ export declare const SETTINGS_SCHEMA: {
3135
3160
  readonly value: "duckduckgo";
3136
3161
  readonly label: "DuckDuckGo";
3137
3162
  readonly description: "Keyless default \u2014 no API key or OAuth required";
3163
+ }, {
3164
+ readonly value: "insane";
3165
+ readonly label: "Insane";
3166
+ readonly description: "Keyless safe public-route fallback inspired by upstream insane-search";
3138
3167
  }, {
3139
3168
  readonly value: "exa";
3140
3169
  readonly label: "Exa";
@@ -3574,6 +3603,14 @@ export interface NotificationsSettings {
3574
3603
  botToken: string | undefined;
3575
3604
  chatId: string | undefined;
3576
3605
  };
3606
+ discord: {
3607
+ botToken: string | undefined;
3608
+ channelId: string | undefined;
3609
+ };
3610
+ slack: {
3611
+ botToken: string | undefined;
3612
+ channelId: string | undefined;
3613
+ };
3577
3614
  redact: boolean;
3578
3615
  daemon: {
3579
3616
  idleTimeoutMs: number;
@@ -172,6 +172,7 @@ export interface AutoCompactionEndEvent {
172
172
  errorMessage?: string;
173
173
  /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
174
174
  skipped?: boolean;
175
+ continuationSkipReason?: "auto_continue_disabled_non_resumable_tail";
175
176
  }
176
177
  /** Fired when auto-retry starts */
177
178
  export interface AutoRetryStartEvent {
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N> --artifact
14
14
  * <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
15
- * / Critic / revision / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
15
+ * / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
16
16
  * an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
17
17
  * the HUD chip to reflect the latest persisted stage.
18
18
  */
@@ -224,6 +224,8 @@ export interface ServerConfig {
224
224
  /** Per-server warmup timeout in milliseconds. Overrides the global WARMUP_TIMEOUT_MS for this server during startup. */
225
225
  warmupTimeoutMs?: number;
226
226
  capabilities?: ServerCapabilities;
227
+ /** Names of lower-precedence servers to drop when this server is available. */
228
+ supersedes?: string[];
227
229
  /** If true, this is a linter/formatter server (e.g., Biome) - used only for diagnostics/actions, not type intelligence */
228
230
  isLinter?: boolean;
229
231
  /** Resolved absolute path to the command binary (set during config loading) */
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Process-wide registry mapping a session id to a sink that delivers a local
3
+ * file to the session's connected Telegram chat as a document. Registered by
4
+ * the notifications extension; consumed by the telegram_send tool.
5
+ */
6
+ /** Delivers a local file to the session's Telegram chat. */
7
+ export type TelegramFileSink = (file: {
8
+ path: string;
9
+ caption?: string;
10
+ }) => Promise<{
11
+ ok: boolean;
12
+ error?: string;
13
+ }>;
14
+ /** Register `sink` for `sessionId`. Returns a disposer that clears it. */
15
+ export declare function registerTelegramFileSink(sessionId: string, sink: TelegramFileSink): () => void;
16
+ /** The Telegram file sink for `sessionId`, if one is registered. */
17
+ export declare function getTelegramFileSink(sessionId: string): TelegramFileSink | undefined;
@@ -0,0 +1,9 @@
1
+ import type { NotificationPresentationAdapter } from "./engine";
2
+ type AdapterKind = "discord" | "slack";
3
+ interface ChatAdapterOptions {
4
+ kind: AdapterKind;
5
+ channelId?: string;
6
+ }
7
+ export declare function createDiscordAdapter(opts?: Omit<ChatAdapterOptions, "kind">): NotificationPresentationAdapter;
8
+ export declare function createSlackAdapter(opts?: Omit<ChatAdapterOptions, "kind">): NotificationPresentationAdapter;
9
+ export {};
@@ -3,13 +3,21 @@ export interface NotificationConfig {
3
3
  enabled: boolean;
4
4
  botToken?: string;
5
5
  chatId?: string;
6
+ discord: {
7
+ botToken?: string;
8
+ channelId?: string;
9
+ };
10
+ slack: {
11
+ botToken?: string;
12
+ channelId?: string;
13
+ };
6
14
  redact: boolean;
7
15
  verbosity: "lean" | "verbose";
8
16
  idleTimeoutMs: number;
9
17
  }
10
18
  /** Read typed config from Settings. */
11
19
  export declare function getNotificationConfig(settings: Settings): NotificationConfig;
12
- /** Is global config sufficient for auto-on (enabled + botToken + chatId all present)? */
20
+ /** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
13
21
  export declare function isGloballyConfigured(cfg: NotificationConfig): boolean;
14
22
  /** Resolve whether the notifications extension should be registered at SDK startup. */
15
23
  export declare function shouldRegisterNotificationsExtension(input: {
@@ -0,0 +1,59 @@
1
+ import { type RedactableAction } from "./config";
2
+ export type NotificationEvent = ({
3
+ type: "action_needed";
4
+ } & RedactableAction) | {
5
+ type: "action_resolved";
6
+ id: string;
7
+ sessionId: string;
8
+ resolvedBy?: string;
9
+ } | {
10
+ type: "frame";
11
+ sessionId: string;
12
+ frame: Record<string, unknown>;
13
+ };
14
+ export interface NotificationReplyRoute {
15
+ sessionId: string;
16
+ actionId: string;
17
+ answer: number | string | {
18
+ selected?: Array<number | string>;
19
+ custom?: string;
20
+ };
21
+ }
22
+ export interface NotificationAdapterPayload {
23
+ adapter: string;
24
+ channelKey?: string;
25
+ body: unknown;
26
+ route?: Omit<NotificationReplyRoute, "answer">;
27
+ }
28
+ export interface NotificationPresentationAdapter {
29
+ readonly kind: "telegram" | "discord" | "slack";
30
+ render(event: NotificationEvent): NotificationAdapterPayload[];
31
+ mapInbound(input: unknown): NotificationReplyRoute | undefined;
32
+ }
33
+ export interface EngineSessionSink {
34
+ sendReply(route: NotificationReplyRoute): void;
35
+ }
36
+ export interface NotificationEngineOptions {
37
+ redact: boolean;
38
+ sessionTag: (sessionId: string) => string;
39
+ }
40
+ /**
41
+ * Shared presentation engine for managed notification clients.
42
+ *
43
+ * It owns fanout, redaction boundaries, pending-action routing, and reply
44
+ * delivery into session sinks. Transport adapters stay pure: render an internal
45
+ * event into a public-safe payload and map an inbound transport interaction
46
+ * back into a session/action answer.
47
+ */
48
+ export declare class NotificationPresentationEngine {
49
+ private readonly opts;
50
+ readonly adapters: readonly NotificationPresentationAdapter[];
51
+ private readonly sessions;
52
+ private readonly pending;
53
+ constructor(adapters: readonly NotificationPresentationAdapter[], opts: NotificationEngineOptions);
54
+ connectSession(sessionId: string, sink: EngineSessionSink): void;
55
+ dropSession(sessionId: string): void;
56
+ fanout(event: NotificationEvent): NotificationAdapterPayload[];
57
+ routeInbound(adapterKind: NotificationPresentationAdapter["kind"], input: unknown): boolean;
58
+ private redactEvent;
59
+ }
@@ -0,0 +1,48 @@
1
+ import type { Settings } from "../config/settings";
2
+ import { RateLimitPool } from "./rate-limit-pool";
3
+ import { type ThreadedSend } from "./threaded-render";
4
+ export interface ManagedNotificationDaemonFs {
5
+ readdir(path: string): Promise<string[]>;
6
+ }
7
+ export interface ManagedSessionSocket {
8
+ sessionId: string;
9
+ token: string;
10
+ ws: WebSocket;
11
+ pending: Map<string, {
12
+ sessionId: string;
13
+ actionId: string;
14
+ }>;
15
+ capable: boolean;
16
+ lastPongAt: number;
17
+ awaitingNonce: string | undefined;
18
+ pingTimer: ReturnType<typeof setInterval> | undefined;
19
+ }
20
+ export interface ManagedNotificationDaemonOptions {
21
+ settings: Settings;
22
+ fs: ManagedNotificationDaemonFs;
23
+ WebSocketImpl?: typeof WebSocket;
24
+ now?: () => number;
25
+ setIntervalImpl?: typeof setInterval;
26
+ clearIntervalImpl?: typeof clearInterval;
27
+ rateLimitPool?: RateLimitPool<{
28
+ send: ThreadedSend;
29
+ topicId?: string;
30
+ }>;
31
+ }
32
+ export declare abstract class ManagedNotificationDaemon {
33
+ protected readonly opts: ManagedNotificationDaemonOptions;
34
+ readonly sessions: Map<string, ManagedSessionSocket>;
35
+ readonly pool: RateLimitPool<{
36
+ send: ThreadedSend;
37
+ topicId?: string;
38
+ }>;
39
+ protected constructor(opts: ManagedNotificationDaemonOptions);
40
+ scanRoots(): Promise<void>;
41
+ connectSession(sessionId: string, url: string, token: string): ManagedSessionSocket;
42
+ protected handleSessionMessage(session: ManagedSessionSocket, msg: Record<string, unknown>): Promise<boolean>;
43
+ protected renderFrame(frame: Record<string, unknown>): ThreadedSend | undefined;
44
+ protected dropSession(session: ManagedSessionSocket): void;
45
+ protected abstract readRoots(): Promise<string[]>;
46
+ private sendHello;
47
+ private startLiveness;
48
+ }
@@ -206,6 +206,12 @@ export declare class TelegramNotificationDaemon {
206
206
  private readonly topics;
207
207
  private readonly pool;
208
208
  private readonly seenUpdateIds;
209
+ /** True once the daemon has nudged the user to enable Threaded Mode. */
210
+ private threadedFallbackNoticeSent;
211
+ /** Sessions whose identity header was already sent flat (Threaded Mode off). */
212
+ private readonly flatIdentitySent;
213
+ /** Cached result of whether the paired chat is a private chat (flat-fallback gate). */
214
+ private pairedChatPrivate;
209
215
  private flushTimer;
210
216
  private scanTimer;
211
217
  private scanning;
@@ -251,7 +257,20 @@ export declare class TelegramNotificationDaemon {
251
257
  private ensureTopic;
252
258
  private persistTopics;
253
259
  loadTopics(): Promise<void>;
260
+ private downloadTelegramFile;
261
+ /**
262
+ * Per-session private temp directories (mode 0700) holding inbound non-image
263
+ * attachments. Keyed by session id and reused across transient reconnects;
264
+ * removed when the daemon stops (see {@link cleanupAllAttachmentDirs}).
265
+ */
266
+ private readonly attachmentDirs;
267
+ private ensureAttachmentDir;
268
+ private cleanupAllAttachmentDirs;
269
+ private resolveInboundAttachment;
254
270
  private flushPool;
271
+ private deliverFlatFallback;
272
+ private pairedChatIsPrivate;
273
+ private notifyThreadedFallback;
255
274
  private startFlushTimer;
256
275
  private stopFlushTimer;
257
276
  private runScan;
@@ -20,12 +20,30 @@ export interface InboundUpdate {
20
20
  message?: {
21
21
  message_id?: unknown;
22
22
  text?: unknown;
23
+ caption?: unknown;
24
+ photo?: unknown;
25
+ document?: unknown;
26
+ video?: unknown;
27
+ audio?: unknown;
28
+ voice?: unknown;
29
+ animation?: unknown;
23
30
  chat?: {
24
31
  id?: unknown;
25
32
  };
26
33
  message_thread_id?: unknown;
27
34
  };
28
35
  }
36
+ /** A downloadable media attachment referenced by an inbound message. */
37
+ export interface InboundAttachment {
38
+ /** Telegram file_id to resolve via getFile. */
39
+ fileId: string;
40
+ /** Source media kind; "photo" is always an image. */
41
+ kind: "photo" | "document" | "video" | "audio" | "voice" | "animation";
42
+ /** MIME type when Telegram provides one. */
43
+ mime?: string;
44
+ /** Original file name when provided. */
45
+ fileName?: string;
46
+ }
29
47
  /** Context for {@link decideThreadedInbound}. All lookups are injected. */
30
48
  export interface ThreadedInboundCtx {
31
49
  /** The single paired chat id (string-compared). */
@@ -43,6 +61,7 @@ export type ThreadedInboundDecision = {
43
61
  updateId: number;
44
62
  threadId: string;
45
63
  messageId?: number;
64
+ attachment?: InboundAttachment;
46
65
  } | {
47
66
  kind: "duplicate";
48
67
  updateId: number;
@@ -11,15 +11,19 @@
11
11
  import type { RateLimitLane } from "./rate-limit-pool";
12
12
  /** A Telegram send derived from a threaded frame (topic id is applied by the daemon). */
13
13
  export interface ThreadedSend {
14
- method: "sendMessage" | "sendPhoto";
14
+ method: "sendMessage" | "sendPhoto" | "sendDocument";
15
15
  /** Rate-limit lane for prioritisation/fairness. */
16
16
  lane: RateLimitLane;
17
17
  /** Message text (sendMessage) or photo caption (sendPhoto). */
18
18
  text?: string;
19
19
  /** Base64 image bytes for sendPhoto. */
20
20
  photoBase64?: string;
21
+ /** Base64 file bytes for sendDocument. */
22
+ documentBase64?: string;
21
23
  /** Image MIME type for sendPhoto. */
22
24
  mime?: string;
25
+ /** Suggested document filename. */
26
+ fileName?: string;
23
27
  /** Coalesce key for live edits (same key collapses to the latest). */
24
28
  coalesceKey?: string;
25
29
  /** True for the one-time identity header (the daemon pins it once). */
@@ -45,6 +49,7 @@ interface ThreadedFrame {
45
49
  data?: unknown;
46
50
  mime?: unknown;
47
51
  caption?: unknown;
52
+ name?: unknown;
48
53
  verbosity?: unknown;
49
54
  redact?: unknown;
50
55
  }
@@ -81,6 +81,7 @@ import type { BranchSummaryEntry, NewSessionOptions, SessionContext, SessionMana
81
81
  import { ToolChoiceQueue } from "./tool-choice-queue";
82
82
  import { YieldQueue } from "./yield-queue";
83
83
  /** Session-specific events that extend the core AgentEvent */
84
+ export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
84
85
  export type AgentSessionEvent = AgentEvent | {
85
86
  type: "auto_compaction_start";
86
87
  reason: "threshold" | "overflow" | "idle";
@@ -94,6 +95,7 @@ export type AgentSessionEvent = AgentEvent | {
94
95
  errorMessage?: string;
95
96
  /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
96
97
  skipped?: boolean;
98
+ continuationSkipReason?: AutoCompactionContinuationSkipReason;
97
99
  } | {
98
100
  type: "auto_retry_start";
99
101
  attempt: number;
@@ -1,8 +1,10 @@
1
1
  import type { AgentToolResult } from "@gajae-code/agent-core";
2
2
  import { type Component } from "@gajae-code/tui";
3
+ import type { Settings } from "../config/settings";
3
4
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
4
5
  import { type Theme } from "../modes/theme/theme";
5
6
  import type { ToolSession } from "../sdk";
7
+ import type { RenderResult } from "../web/scrapers/types";
6
8
  import { type OutputMeta } from "./output-meta";
7
9
  export declare function isReadableUrlPath(value: string): boolean;
8
10
  export interface ParsedReadUrlTarget {
@@ -16,6 +18,27 @@ interface FetchImagePayload {
16
18
  data: string;
17
19
  mimeType: string;
18
20
  }
21
+ type FetchRenderResult = RenderResult & {
22
+ image?: FetchImagePayload;
23
+ };
24
+ /**
25
+ * Opt-in insane-search fallback for blocked / degraded public URL reads.
26
+ *
27
+ * Returns a finalized `method: "insane"` result on success, or null (so the
28
+ * caller continues with its normal degraded behavior). Fail-closed: no note,
29
+ * guard DNS, dependency probe, or subprocess when raw mode or the opt-in
30
+ * setting is off. The public-URL guard runs BEFORE any probe/spawn.
31
+ */
32
+ export declare function tryInsaneFallback(args: {
33
+ url: string;
34
+ finalUrl: string;
35
+ timeout: number;
36
+ raw: boolean;
37
+ settings: Settings;
38
+ signal: AbortSignal | undefined;
39
+ fetchedAt: string;
40
+ notes: string[];
41
+ }): Promise<FetchRenderResult | null>;
19
42
  export interface ReadUrlToolDetails {
20
43
  kind: "url";
21
44
  url: string;
@@ -55,6 +55,7 @@ export * from "./search-tool-bm25";
55
55
  export * from "./skill";
56
56
  export * from "./ssh";
57
57
  export * from "./subagent";
58
+ export * from "./telegram-send";
58
59
  export * from "./todo-write";
59
60
  export * from "./vim";
60
61
  export * from "./write";
@@ -0,0 +1,32 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
+ import { z } from "zod/v4";
3
+ import type { ToolSession } from "./index";
4
+ declare const telegramSendSchema: z.ZodObject<{
5
+ path: z.ZodString;
6
+ caption: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ type TelegramSendParams = z.infer<typeof telegramSendSchema>;
9
+ interface TelegramSendDetails {
10
+ path: string;
11
+ caption?: string;
12
+ ok: boolean;
13
+ error?: string;
14
+ }
15
+ export declare class TelegramSendTool implements AgentTool<typeof telegramSendSchema, TelegramSendDetails> {
16
+ private readonly session;
17
+ readonly name = "telegram_send";
18
+ readonly label = "TelegramSend";
19
+ readonly summary = "Send a workspace file to Telegram";
20
+ readonly loadMode = "discoverable";
21
+ readonly description: string;
22
+ readonly parameters: z.ZodObject<{
23
+ path: z.ZodString;
24
+ caption: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strip>;
26
+ readonly strict = true;
27
+ constructor(session: ToolSession);
28
+ static createIf(session: ToolSession): TelegramSendTool | null;
29
+ private resolveContainedFile;
30
+ execute(_toolCallId: string, params: TelegramSendParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<TelegramSendDetails>, _context?: AgentToolContext): Promise<AgentToolResult<TelegramSendDetails>>;
31
+ }
32
+ export {};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Bridge from TypeScript to the vendored insane-search Python engine.
3
+ *
4
+ * Invokes `python3 -m engine "<url>" --json` per fallback attempt (cwd + PYTHONPATH
5
+ * pointed at the vendored engine), validates the JSON envelope, and maps it onto a
6
+ * discriminated result. Hardened: clamped timeout, AbortSignal propagation that
7
+ * kills+reaps the child, bounded stdout/stderr capture, and a per-process
8
+ * concurrency cap so blocked reads cannot fork-storm.
9
+ *
10
+ * Fail-closed: missing dependencies / bad output / auth-required never throw past
11
+ * the caller and never auto-install anything; they return ok:false with a stable,
12
+ * bounded note so `read` can continue with its normal degraded result.
13
+ */
14
+ import { spawn as nodeSpawn } from "node:child_process";
15
+ /** packages/coding-agent/vendor/insane-search */
16
+ export declare const INSANE_VENDOR_DIR: string;
17
+ /** Stable note prefixes — tests assert on these without depending on full stderr. */
18
+ export declare const INSANE_NOTES: {
19
+ readonly guardBlocked: (reason: string) => string;
20
+ readonly vendorMissing: `insane fallback unavailable: vendor engine missing at packages/coding-agent/vendor/insane-search`;
21
+ readonly noPython: `insane fallback unavailable: python3 not found; install python3 and curl_cffi, then retry with web.insaneFallback=true`;
22
+ readonly noCurlCffi: `insane fallback unavailable: python3 cannot import curl_cffi; install curl_cffi for Phase 0-2`;
23
+ readonly noBrowser: `insane fallback unavailable: node/playwright/stealth dependencies missing for Phase 3; install dependencies under packages/coding-agent/vendor/insane-search/engine/templates`;
24
+ readonly timeout: (seconds: number) => string;
25
+ readonly invalidJson: `insane fallback failed: engine returned invalid JSON`;
26
+ readonly authRequired: `insane fallback stopped: authentication required`;
27
+ readonly verdict: (verdict: string) => string;
28
+ readonly untried: (routes: string) => string;
29
+ readonly mustBrowserMcp: `insane fallback requires browser MCP/manual phase: must_invoke_playwright_mcp=true`;
30
+ readonly concurrency: `insane fallback skipped: max concurrent engine attempts reached`;
31
+ readonly emptyContent: `insane fallback failed: engine reported ok but returned no content`;
32
+ };
33
+ /** Raw JSON envelope produced by `python3 -m engine --json`. */
34
+ export interface InsaneFetchResultRaw {
35
+ ok?: boolean;
36
+ verdict?: string;
37
+ content?: string;
38
+ profile_used?: string;
39
+ trace?: unknown;
40
+ untried_routes?: string[];
41
+ must_invoke_playwright_mcp?: boolean;
42
+ }
43
+ export interface InsaneSuccess {
44
+ ok: true;
45
+ content: string;
46
+ profileUsed?: string;
47
+ notes: string[];
48
+ }
49
+ export interface InsaneFailure {
50
+ ok: false;
51
+ reason: string;
52
+ verdict?: string;
53
+ notes: string[];
54
+ }
55
+ export type InsaneBridgeResult = InsaneSuccess | InsaneFailure;
56
+ export interface EngineInvocation {
57
+ url: string;
58
+ timeoutMs: number;
59
+ signal?: AbortSignal;
60
+ }
61
+ export interface EngineRawOutput {
62
+ code: number | null;
63
+ stdout: string;
64
+ stderr: string;
65
+ timedOut: boolean;
66
+ aborted: boolean;
67
+ }
68
+ /** Seam: run the engine subprocess. Default spawns python3. */
69
+ export type EngineRunner = (inv: EngineInvocation) => Promise<EngineRawOutput>;
70
+ export interface InsaneDependencyStatus {
71
+ vendorPresent: boolean;
72
+ python: boolean;
73
+ curlCffi: boolean;
74
+ browser: boolean;
75
+ }
76
+ /** Seam: probe dependencies. Default probes the real environment (cached). */
77
+ export type DependencyProber = () => Promise<InsaneDependencyStatus>;
78
+ type SpawnImpl = typeof nodeSpawn;
79
+ /** Real engine runner: `python3 -m engine "<url>" --json`. */
80
+ export declare function runEngineSubprocess(inv: EngineInvocation, options?: {
81
+ spawnImpl?: SpawnImpl;
82
+ }): Promise<EngineRawOutput>;
83
+ /** Reset the probe cache between tests so probe state never leaks. */
84
+ export declare function resetInsaneProbeCacheForTest(): void;
85
+ /** Probe (and cache) the insane-search runtime dependencies. */
86
+ export declare function probeInsaneDependencies(): Promise<InsaneDependencyStatus>;
87
+ export declare function resetInsaneConcurrencyForTest(): void;
88
+ export interface TryInsaneFetchOptions {
89
+ timeoutMs?: number;
90
+ signal?: AbortSignal;
91
+ concurrencyLimit?: number;
92
+ /** Seam: dependency prober (default real, cached). */
93
+ prober?: DependencyProber;
94
+ /** Seam: engine runner (default real subprocess). */
95
+ runner?: EngineRunner;
96
+ }
97
+ /**
98
+ * Attempt to read `url` through the insane-search engine. The caller is
99
+ * responsible for the opt-in gate, raw-mode skip, and the public-URL guard
100
+ * (which MUST run before this is called). Never throws; always returns a result.
101
+ */
102
+ export declare function tryInsaneFetch(url: string, options?: TryInsaneFetchOptions): Promise<InsaneBridgeResult>;
103
+ export {};
@@ -0,0 +1,22 @@
1
+ export interface PublicUrlAccepted {
2
+ ok: true;
3
+ url: URL;
4
+ addresses: string[];
5
+ }
6
+ export interface PublicUrlRejected {
7
+ ok: false;
8
+ reason: string;
9
+ }
10
+ export type PublicUrlResult = PublicUrlAccepted | PublicUrlRejected;
11
+ /** Resolver seam so tests can inject DNS results without real lookups. */
12
+ export type AddressResolver = (hostname: string) => Promise<string[]>;
13
+ /** True for any address that is not a routable public unicast address. */
14
+ export declare function isPrivateOrSpecialAddress(address: string): boolean;
15
+ /**
16
+ * Validate that `rawUrl` is a public http/https target safe to hand to the
17
+ * insane-search engine. Resolves DNS names and rejects any that map to a
18
+ * private/special address. Never throws; returns a discriminated result.
19
+ */
20
+ export declare function validatePublicHttpUrlForInsane(rawUrl: string, options?: {
21
+ resolver?: AddressResolver;
22
+ }): Promise<PublicUrlResult>;