@sayknow-cli/coding-agent 0.2.7 → 0.3.1
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.
- package/CHANGELOG.md +20 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +25 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +136 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +69 -0
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +295 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +77 -0
- package/dist/types/notifications/threaded-render.d.ts +71 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +41 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/state-writer.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +19 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +22 -0
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +10 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/args.ts +1 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +421 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +143 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/skc/rules/ponytail.md +68 -0
- package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/components/settings-defs.ts +2 -1
- package/src/modes/components/settings-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +35 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +52 -4
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/theme/theme.ts +4 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +128 -0
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +842 -0
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1591 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +136 -0
- package/src/notifications/threaded-render.ts +173 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +195 -54
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skc-runtime/deep-interview-recorder.ts +2 -2
- package/src/skc-runtime/launch-tmux.ts +27 -5
- package/src/skc-runtime/ledger-event-renderer.ts +1 -0
- package/src/skc-runtime/ralplan-runtime.ts +2 -2
- package/src/skc-runtime/state-runtime.ts +18 -10
- package/src/skc-runtime/state-writer.ts +8 -8
- package/src/skc-runtime/tmux-common.ts +8 -0
- package/src/skc-runtime/tmux-sessions.ts +8 -1
- package/src/skc-runtime/ultragoal-guard.ts +57 -2
- package/src/skc-runtime/ultragoal-runtime.ts +105 -19
- package/src/skc-runtime/workflow-manifest.generated.json +56 -2
- package/src/skc-runtime/workflow-manifest.ts +18 -3
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/fetch.ts +78 -1
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +22 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
- package/src/tools/telegram-send.ts +137 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +155 -0
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-wide shared Telegram rate-limit pool for the threaded session surface.
|
|
3
|
+
*
|
|
4
|
+
* Multiple SKC sessions on one host share a single bot token and paired chat.
|
|
5
|
+
* Telegram enforces per-bot/per-chat limits (~1 message/sec, bursts up to ~20),
|
|
6
|
+
* so the singleton notifications daemon owns ONE pool that all per-session
|
|
7
|
+
* threads draw from. The pool provides:
|
|
8
|
+
*
|
|
9
|
+
* - a token bucket (burst capacity + steady refill) modelling the chat limit;
|
|
10
|
+
* - priority lanes (`ask` > `finalized` > `live` > `idle`) so urgent frames
|
|
11
|
+
* win scarce tokens;
|
|
12
|
+
* - per-session round-robin fairness within a lane so one session's live-edit
|
|
13
|
+
* stream cannot starve other sessions;
|
|
14
|
+
* - coalescing of live edits that share a `coalesceKey` (the latest rendered
|
|
15
|
+
* text replaces the queued one) so throttled edit storms collapse.
|
|
16
|
+
*
|
|
17
|
+
* The core is a pull-based scheduler with an injectable clock so fairness,
|
|
18
|
+
* starvation, and burst behaviour are deterministically unit-testable without
|
|
19
|
+
* real time or a live Bot API.
|
|
20
|
+
*/
|
|
21
|
+
/** Delivery lanes in descending priority. */
|
|
22
|
+
export type RateLimitLane = "ask" | "finalized" | "live" | "idle";
|
|
23
|
+
/** Lanes ordered from highest to lowest priority. */
|
|
24
|
+
export declare const LANE_PRIORITY: readonly RateLimitLane[];
|
|
25
|
+
/** A unit of work competing for a send slot. */
|
|
26
|
+
export interface RateLimitItem<T = unknown> {
|
|
27
|
+
/** Owning session id (used for per-session fairness). */
|
|
28
|
+
sessionId: string;
|
|
29
|
+
/** Priority lane. */
|
|
30
|
+
lane: RateLimitLane;
|
|
31
|
+
/**
|
|
32
|
+
* Optional coalesce key. Submitting another item with the same
|
|
33
|
+
* `(sessionId, lane, coalesceKey)` replaces the queued payload with the
|
|
34
|
+
* newer one instead of enqueuing a duplicate (used for live edits).
|
|
35
|
+
*/
|
|
36
|
+
coalesceKey?: string;
|
|
37
|
+
/** Opaque payload the caller maps to an actual Telegram send. */
|
|
38
|
+
payload: T;
|
|
39
|
+
}
|
|
40
|
+
/** Options for {@link RateLimitPool}. */
|
|
41
|
+
export interface RateLimitPoolOptions {
|
|
42
|
+
/** Burst capacity (max tokens). Default 20 (Telegram per-chat burst). */
|
|
43
|
+
capacity?: number;
|
|
44
|
+
/** Steady refill rate in tokens per second. Default 1 (~1 msg/sec/chat). */
|
|
45
|
+
refillPerSec?: number;
|
|
46
|
+
/** Injectable clock in ms. Default `Date.now`. */
|
|
47
|
+
now?: () => number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A deterministic, pull-based shared rate-limit scheduler.
|
|
51
|
+
*
|
|
52
|
+
* Callers {@link submit} work and periodically {@link drain} (e.g. on a timer
|
|
53
|
+
* or after each submit); `drain` returns the items granted a send slot, in the
|
|
54
|
+
* order they should be sent.
|
|
55
|
+
*/
|
|
56
|
+
export declare class RateLimitPool<T = unknown> {
|
|
57
|
+
private readonly capacity;
|
|
58
|
+
private readonly refillPerSec;
|
|
59
|
+
private readonly now;
|
|
60
|
+
/** Per-lane FIFO queues; each lane holds items across sessions. */
|
|
61
|
+
private readonly lanes;
|
|
62
|
+
/** Rotating session cursor per lane for round-robin fairness. */
|
|
63
|
+
private readonly laneCursor;
|
|
64
|
+
private tokens;
|
|
65
|
+
private lastRefill;
|
|
66
|
+
private seqCounter;
|
|
67
|
+
constructor(options?: RateLimitPoolOptions);
|
|
68
|
+
/** Number of items currently queued across all lanes. */
|
|
69
|
+
get pending(): number;
|
|
70
|
+
/** Current available token count (after refill at `now`). */
|
|
71
|
+
availableTokens(nowMs?: number): number;
|
|
72
|
+
/**
|
|
73
|
+
* Submit an item. If it carries a `coalesceKey` matching a queued item in
|
|
74
|
+
* the same `(sessionId, lane)`, the queued payload is replaced (latest
|
|
75
|
+
* wins) and FIFO position is preserved; otherwise it is appended.
|
|
76
|
+
*/
|
|
77
|
+
submit(item: RateLimitItem<T>): void;
|
|
78
|
+
/**
|
|
79
|
+
* Grant as many queued items as tokens allow at `nowMs`. Items are selected
|
|
80
|
+
* by lane priority, then round-robin across sessions within a lane (so no
|
|
81
|
+
* single session monopolises a lane), consuming one token each.
|
|
82
|
+
*/
|
|
83
|
+
drain(nowMs?: number): RateLimitItem<T>[];
|
|
84
|
+
private refill;
|
|
85
|
+
/** Pop the next item by lane priority + per-session round-robin fairness. */
|
|
86
|
+
private takeNext;
|
|
87
|
+
/**
|
|
88
|
+
* Choose the index to serve from a lane queue using round-robin over the
|
|
89
|
+
* distinct session ids present, starting just after the last-served
|
|
90
|
+
* session. Falls back to FIFO (index 0) when only one session is queued.
|
|
91
|
+
*/
|
|
92
|
+
private pickFairIndex;
|
|
93
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Reference CLI for the notifications SDK Telegram client.
|
|
4
|
+
*
|
|
5
|
+
* Bridges a running SKC session's notification endpoint to a Telegram bot so you
|
|
6
|
+
* can answer asks / see idle pings from your phone — no RPC mode required. This
|
|
7
|
+
* is an EXAMPLE/template (the SDK contract is in `docs/notifications-sdk.md`);
|
|
8
|
+
* Discord/Slack clients are written the same way.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* bun run packages/coding-agent/src/notifications/telegram-cli.ts \
|
|
12
|
+
* --bot-token <token> [--chat-id <id>] [--endpoint-file <path> | --session-id <id>] [--repo <dir>]
|
|
13
|
+
*
|
|
14
|
+
* Env fallbacks: SKC_TG_BOT_TOKEN, SKC_TG_CHAT_ID.
|
|
15
|
+
* If --chat-id is omitted it is auto-resolved from getUpdates (message the bot once).
|
|
16
|
+
* If neither --endpoint-file nor --session-id is given, the newest endpoint file
|
|
17
|
+
* under <repo>/.skc/state/notifications/ is used.
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Settings } from "../config/settings";
|
|
2
|
+
import { TelegramNotificationDaemon } from "./telegram-daemon";
|
|
3
|
+
export interface RunDaemonInternalDeps {
|
|
4
|
+
SettingsImpl?: Pick<typeof Settings, "init">;
|
|
5
|
+
DaemonImpl?: typeof TelegramNotificationDaemon;
|
|
6
|
+
processPid?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function runDaemonSmoke(opts?: {
|
|
9
|
+
agentDir?: string;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function runDaemonInternal(argv: string[], deps?: RunDaemonInternalDeps): Promise<void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram daemon controller + owner-scoped control-request helpers.
|
|
3
|
+
*
|
|
4
|
+
* Reload is a hybrid: an owner-scoped control-request file records auditable
|
|
5
|
+
* intent, SIGTERM is the wakeup that aborts the in-flight long poll, and a
|
|
6
|
+
* fresh daemon is spawned only after the old pid is dead / has exited. This
|
|
7
|
+
* keeps the single-poller invariant (no Telegram getUpdates 409 overlap) and
|
|
8
|
+
* never steals a still-live owner.
|
|
9
|
+
*/
|
|
10
|
+
import type { Settings } from "../config/settings";
|
|
11
|
+
import type { BuiltInDaemonController, DaemonOperationOptions, DaemonOperationResult, DaemonStatus } from "../daemon/control-types";
|
|
12
|
+
import { type TelegramDaemonDeps, type TelegramDaemonFs } from "./telegram-daemon";
|
|
13
|
+
export interface TelegramDaemonControlRequest {
|
|
14
|
+
version: 1;
|
|
15
|
+
requestId: string;
|
|
16
|
+
action: "reload" | "stop";
|
|
17
|
+
ownerId: string;
|
|
18
|
+
pid: number;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function telegramControlRequestPath(agentDir: string): string;
|
|
22
|
+
export declare function readTelegramControlRequest(settings: Settings, fsImpl?: TelegramDaemonFs): Promise<TelegramDaemonControlRequest | undefined>;
|
|
23
|
+
export declare function writeTelegramControlRequest(settings: Settings, request: TelegramDaemonControlRequest, fsImpl?: TelegramDaemonFs): Promise<void>;
|
|
24
|
+
export declare function clearTelegramControlRequest(settings: Settings, requestId?: string, fsImpl?: TelegramDaemonFs): Promise<void>;
|
|
25
|
+
export interface TelegramDaemonControlDeps {
|
|
26
|
+
fs?: TelegramDaemonFs;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
pidAlive?: (pid: number) => boolean;
|
|
29
|
+
sendSignal?: (pid: number, signal: NodeJS.Signals) => void;
|
|
30
|
+
spawn?: TelegramDaemonDeps["spawn"];
|
|
31
|
+
execPath?: string;
|
|
32
|
+
randomId?: () => string;
|
|
33
|
+
sleep?: (ms: number) => Promise<void>;
|
|
34
|
+
waitStepMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare class TelegramDaemonController implements BuiltInDaemonController {
|
|
37
|
+
private readonly settings;
|
|
38
|
+
private readonly deps;
|
|
39
|
+
readonly kind: "telegram";
|
|
40
|
+
private readonly fsImpl;
|
|
41
|
+
private readonly now;
|
|
42
|
+
private readonly pidAlive;
|
|
43
|
+
private readonly sendSignal;
|
|
44
|
+
private readonly waitStepMs;
|
|
45
|
+
constructor(settings: Settings, deps?: TelegramDaemonControlDeps);
|
|
46
|
+
private runtimeInfo;
|
|
47
|
+
status(): Promise<DaemonStatus>;
|
|
48
|
+
private spawnDeps;
|
|
49
|
+
private sleep;
|
|
50
|
+
private waitForPidDeath;
|
|
51
|
+
private result;
|
|
52
|
+
reload(opts?: DaemonOperationOptions): Promise<DaemonOperationResult>;
|
|
53
|
+
stop(opts?: DaemonOperationOptions): Promise<DaemonOperationResult>;
|
|
54
|
+
private stopOrReload;
|
|
55
|
+
private clearOwnRequest;
|
|
56
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { Settings } from "../config/settings";
|
|
3
|
+
import type { DaemonRuntimeInfo } from "../daemon/control-types";
|
|
4
|
+
import { type AliasTable, type CallbackRoute, type PendingAsk } from "./telegram-reference";
|
|
5
|
+
export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled";
|
|
6
|
+
export interface DaemonState {
|
|
7
|
+
pid: number;
|
|
8
|
+
ownerId: string;
|
|
9
|
+
tokenFingerprint: string;
|
|
10
|
+
chatId: string;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
heartbeatAt: number;
|
|
13
|
+
roots: string[];
|
|
14
|
+
version: 1;
|
|
15
|
+
stoppedAt?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface DaemonPaths {
|
|
18
|
+
dir: string;
|
|
19
|
+
lock: string;
|
|
20
|
+
state: string;
|
|
21
|
+
roots: string;
|
|
22
|
+
steal: string;
|
|
23
|
+
aliases: string;
|
|
24
|
+
}
|
|
25
|
+
export interface TelegramDaemonFs {
|
|
26
|
+
mkdir(path: string, opts?: fs.MakeDirectoryOptions): Promise<void>;
|
|
27
|
+
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
28
|
+
writeFile(path: string, data: string, opts?: fs.WriteFileOptions): Promise<void>;
|
|
29
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
30
|
+
unlink(path: string): Promise<void>;
|
|
31
|
+
open(path: string, flags: string, mode?: number): Promise<{
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}>;
|
|
34
|
+
readdir(path: string): Promise<string[]>;
|
|
35
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface SpawnResult {
|
|
38
|
+
unref?: () => void;
|
|
39
|
+
}
|
|
40
|
+
export interface TelegramDaemonDeps {
|
|
41
|
+
fs?: TelegramDaemonFs;
|
|
42
|
+
now?: () => number;
|
|
43
|
+
pid?: number;
|
|
44
|
+
pidAlive?: (pid: number) => boolean;
|
|
45
|
+
spawn?: (command: string, args: string[], opts: {
|
|
46
|
+
detached: boolean;
|
|
47
|
+
stdio: "ignore";
|
|
48
|
+
logPath?: string;
|
|
49
|
+
}) => SpawnResult;
|
|
50
|
+
execPath?: string;
|
|
51
|
+
randomId?: () => string;
|
|
52
|
+
}
|
|
53
|
+
export declare const HEARTBEAT_INTERVAL_MS = 5000;
|
|
54
|
+
export declare const HEARTBEAT_TTL_MS = 20000;
|
|
55
|
+
export declare const DAEMON_VERSION = 1;
|
|
56
|
+
/** Capability token advertised when the server supports app-level ping/pong. */
|
|
57
|
+
export declare const CLIENT_PING_PONG_CAPABILITY = "client_ping_pong";
|
|
58
|
+
/** Protocol version the daemon advertises in its ClientHello. */
|
|
59
|
+
export declare const NOTIFICATION_PROTOCOL_VERSION = 2;
|
|
60
|
+
export declare function daemonPaths(agentDir: string): DaemonPaths;
|
|
61
|
+
export declare function registerNotificationRoot(input: {
|
|
62
|
+
settings: Settings;
|
|
63
|
+
cwd: string;
|
|
64
|
+
sessionId: string;
|
|
65
|
+
fs?: TelegramDaemonFs;
|
|
66
|
+
}): Promise<string>;
|
|
67
|
+
export declare function isFreshLiveOwner(input: {
|
|
68
|
+
state: DaemonState | undefined;
|
|
69
|
+
now: number;
|
|
70
|
+
tokenFingerprint: string;
|
|
71
|
+
chatId: string;
|
|
72
|
+
pidAlive: (pid: number) => boolean;
|
|
73
|
+
}): boolean;
|
|
74
|
+
export declare function acquireDaemonOwnership(input: {
|
|
75
|
+
settings: Settings;
|
|
76
|
+
roots?: string[];
|
|
77
|
+
tokenFingerprint: string;
|
|
78
|
+
chatId: string;
|
|
79
|
+
fs?: TelegramDaemonFs;
|
|
80
|
+
now?: () => number;
|
|
81
|
+
pid?: number;
|
|
82
|
+
pidAlive?: (pid: number) => boolean;
|
|
83
|
+
randomId?: () => string;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
acquired: boolean;
|
|
86
|
+
ownerId?: string;
|
|
87
|
+
attached?: boolean;
|
|
88
|
+
}>;
|
|
89
|
+
export declare function renewDaemonHeartbeat(input: {
|
|
90
|
+
settings: Settings;
|
|
91
|
+
ownerId: string;
|
|
92
|
+
fs?: TelegramDaemonFs;
|
|
93
|
+
now?: () => number;
|
|
94
|
+
pid?: number;
|
|
95
|
+
}): Promise<boolean>;
|
|
96
|
+
export declare function releaseDaemonOwnership(input: {
|
|
97
|
+
settings: Settings;
|
|
98
|
+
ownerId: string;
|
|
99
|
+
fs?: TelegramDaemonFs;
|
|
100
|
+
now?: () => number;
|
|
101
|
+
}): Promise<void>;
|
|
102
|
+
/** Read the persisted daemon ownership state (or undefined when absent). */
|
|
103
|
+
export declare function readDaemonState(settings: Settings, fs?: TelegramDaemonFs): Promise<DaemonState | undefined>;
|
|
104
|
+
/** Read the persisted notification roots list. */
|
|
105
|
+
export declare function readDaemonRoots(settings: Settings, fs?: TelegramDaemonFs): Promise<string[]>;
|
|
106
|
+
export interface TelegramSpawnOwnerInput {
|
|
107
|
+
settings: Settings;
|
|
108
|
+
roots?: string[];
|
|
109
|
+
tokenFingerprint: string;
|
|
110
|
+
chatId: string;
|
|
111
|
+
}
|
|
112
|
+
export interface TelegramSpawnOwnerResult {
|
|
113
|
+
result: EnsureDaemonResult;
|
|
114
|
+
ownerId?: string;
|
|
115
|
+
runtime: DaemonRuntimeInfo;
|
|
116
|
+
warnings: string[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the detached spawn command/args for the daemon-internal entrypoint.
|
|
120
|
+
* Source mode prepends the entry script so the respawn loads edited source;
|
|
121
|
+
* a compiled binary self-spawns its own subcommand directly.
|
|
122
|
+
*/
|
|
123
|
+
export declare function buildTelegramDaemonSpawnArgs(input: {
|
|
124
|
+
execPath?: string;
|
|
125
|
+
ownerId: string;
|
|
126
|
+
agentDir: string;
|
|
127
|
+
}): {
|
|
128
|
+
command: string;
|
|
129
|
+
args: string[];
|
|
130
|
+
runtime: DaemonRuntimeInfo;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Acquire ownership for the given Telegram identity and, if acquired, spawn a
|
|
134
|
+
* fresh detached daemon process. Does NOT register notification roots; callers
|
|
135
|
+
* that own a session (autostart) register roots separately, while reload reuses
|
|
136
|
+
* already-persisted roots.
|
|
137
|
+
*/
|
|
138
|
+
export declare function spawnTelegramDaemonOwner(input: TelegramSpawnOwnerInput, deps?: TelegramDaemonDeps): Promise<TelegramSpawnOwnerResult>;
|
|
139
|
+
export declare function ensureTelegramDaemonRunning(input: {
|
|
140
|
+
settings: Settings;
|
|
141
|
+
cwd: string;
|
|
142
|
+
sessionId: string;
|
|
143
|
+
}, deps?: TelegramDaemonDeps): Promise<EnsureDaemonResult>;
|
|
144
|
+
export interface BotApi {
|
|
145
|
+
call(method: string, body: unknown, opts?: {
|
|
146
|
+
signal?: AbortSignal;
|
|
147
|
+
}): Promise<unknown>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Cooperative control seam for the daemon run loop. Implemented by the
|
|
151
|
+
* daemon-internal CLI / controller against the owner-scoped control-request
|
|
152
|
+
* file so the daemon does not import the control module directly.
|
|
153
|
+
*/
|
|
154
|
+
export interface DaemonControlHooks {
|
|
155
|
+
/** Returns true when a stop/reload has been requested for this owner. */
|
|
156
|
+
shouldStop(ownerId: string): Promise<boolean>;
|
|
157
|
+
/** Clear a consumed control request (best-effort). */
|
|
158
|
+
clear?(ownerId: string): Promise<void>;
|
|
159
|
+
}
|
|
160
|
+
export interface TelegramDaemonOptions {
|
|
161
|
+
settings: Settings;
|
|
162
|
+
ownerId: string;
|
|
163
|
+
botToken: string;
|
|
164
|
+
chatId: string;
|
|
165
|
+
apiBase?: string;
|
|
166
|
+
fetchImpl?: typeof fetch;
|
|
167
|
+
fs?: TelegramDaemonFs;
|
|
168
|
+
WebSocketImpl?: typeof WebSocket;
|
|
169
|
+
now?: () => number;
|
|
170
|
+
setTimeoutImpl?: typeof setTimeout;
|
|
171
|
+
clearTimeoutImpl?: typeof clearTimeout;
|
|
172
|
+
setIntervalImpl?: typeof setInterval;
|
|
173
|
+
clearIntervalImpl?: typeof clearInterval;
|
|
174
|
+
idleTimeoutMs?: number;
|
|
175
|
+
scanIntervalMs?: number;
|
|
176
|
+
pid?: number;
|
|
177
|
+
botApi?: BotApi;
|
|
178
|
+
control?: DaemonControlHooks;
|
|
179
|
+
}
|
|
180
|
+
interface SessionSocket {
|
|
181
|
+
sessionId: string;
|
|
182
|
+
token: string;
|
|
183
|
+
ws: WebSocket;
|
|
184
|
+
pending: Map<string, {
|
|
185
|
+
sessionId: string;
|
|
186
|
+
actionId: string;
|
|
187
|
+
}>;
|
|
188
|
+
/** True once the server advertised the `client_ping_pong` capability. */
|
|
189
|
+
capable: boolean;
|
|
190
|
+
/** Timestamp (via opts.now) of the last received pong; seeds the TTL window. */
|
|
191
|
+
lastPongAt: number;
|
|
192
|
+
/** Nonce of the most recent in-flight ping, if any. */
|
|
193
|
+
awaitingNonce: string | undefined;
|
|
194
|
+
/** Per-session liveness interval handle (only set for capable sessions). */
|
|
195
|
+
pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
196
|
+
}
|
|
197
|
+
export declare class TelegramNotificationDaemon {
|
|
198
|
+
private readonly opts;
|
|
199
|
+
readonly aliasTable: AliasTable;
|
|
200
|
+
readonly messageRoutes: Map<string | number, CallbackRoute | Omit<CallbackRoute, "answer">>;
|
|
201
|
+
readonly sessions: Map<string, SessionSocket>;
|
|
202
|
+
private running;
|
|
203
|
+
private offset;
|
|
204
|
+
private readonly fsImpl;
|
|
205
|
+
private readonly botApi;
|
|
206
|
+
private readonly topics;
|
|
207
|
+
private readonly pool;
|
|
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;
|
|
215
|
+
private flushTimer;
|
|
216
|
+
private scanTimer;
|
|
217
|
+
private scanning;
|
|
218
|
+
private typingTimer;
|
|
219
|
+
/** Sessions whose agent loop is currently busy (drives the typing indicator). */
|
|
220
|
+
private readonly busy;
|
|
221
|
+
/** Inbound update id → originating Telegram message, for delivery reactions. */
|
|
222
|
+
private readonly inboundReactions;
|
|
223
|
+
/** AbortController for the in-flight long poll; aborted by requestStop() to wake the loop. */
|
|
224
|
+
private activePoll;
|
|
225
|
+
/** Set when a cooperative stop has been requested (signal or control request). */
|
|
226
|
+
private stopRequested;
|
|
227
|
+
/** Current bounded backoff after a Telegram getUpdates 409 conflict (0 when healthy). */
|
|
228
|
+
private pollConflictBackoffMs;
|
|
229
|
+
/**
|
|
230
|
+
* Cooperatively stop the daemon: set the stop flag and abort the in-flight
|
|
231
|
+
* long poll so the run loop wakes immediately instead of waiting out the
|
|
232
|
+
* ~25s getUpdates timeout. Safe to call from a signal handler.
|
|
233
|
+
*/
|
|
234
|
+
requestStop(_reason?: "reload" | "stop" | "signal"): void;
|
|
235
|
+
constructor(opts: TelegramDaemonOptions);
|
|
236
|
+
loadAliases(): Promise<void>;
|
|
237
|
+
persistAliases(): Promise<void>;
|
|
238
|
+
scanRoots(): Promise<void>;
|
|
239
|
+
connectSession(sessionId: string, url: string, token: string): void;
|
|
240
|
+
/**
|
|
241
|
+
* Start ack-based liveness for a session whose server advertised the
|
|
242
|
+
* `client_ping_pong` capability. Each interval drops the session when no pong
|
|
243
|
+
* has arrived within the TTL (the half-open case the socket never signals via
|
|
244
|
+
* `close`), otherwise sends a fresh application-level ping. The timer is bound
|
|
245
|
+
* to this exact session object.
|
|
246
|
+
*/
|
|
247
|
+
private startLiveness;
|
|
248
|
+
/**
|
|
249
|
+
* Idempotent, identity-guarded session teardown. Clears the liveness timer,
|
|
250
|
+
* removes the map entry only when it still points at this exact session object
|
|
251
|
+
* (so a delayed old close cannot delete a replacement), and best-effort closes
|
|
252
|
+
* the socket. `scanRoots()` then reconnects the session.
|
|
253
|
+
*/
|
|
254
|
+
private dropSession;
|
|
255
|
+
private static readonly THREADED_FRAMES;
|
|
256
|
+
private topicNameFor;
|
|
257
|
+
private ensureTopic;
|
|
258
|
+
private persistTopics;
|
|
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;
|
|
270
|
+
private flushPool;
|
|
271
|
+
private deliverFlatFallback;
|
|
272
|
+
private pairedChatIsPrivate;
|
|
273
|
+
private notifyThreadedFallback;
|
|
274
|
+
private startFlushTimer;
|
|
275
|
+
private stopFlushTimer;
|
|
276
|
+
private runScan;
|
|
277
|
+
private startScanTimer;
|
|
278
|
+
private stopScanTimer;
|
|
279
|
+
private sendTyping;
|
|
280
|
+
private setReaction;
|
|
281
|
+
private startTypingTimer;
|
|
282
|
+
private stopTypingTimer;
|
|
283
|
+
handleSessionMessage(session: SessionSocket, msg: any): Promise<void>;
|
|
284
|
+
pendingBySession: (sessionId?: string) => PendingAsk[];
|
|
285
|
+
private sendStaleGuidance;
|
|
286
|
+
handleTelegramUpdate(update: unknown): Promise<void>;
|
|
287
|
+
pollOnce(signal?: AbortSignal): Promise<number>;
|
|
288
|
+
/** Abortable sleep honoring the injected timer; resolves early on abort. */
|
|
289
|
+
private sleep;
|
|
290
|
+
/** Sync the bot's Telegram command menu to what the daemon actually handles. */
|
|
291
|
+
registerBotCommands(): Promise<void>;
|
|
292
|
+
run(): Promise<void>;
|
|
293
|
+
private controlStopRequested;
|
|
294
|
+
}
|
|
295
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram **reference** client for the notifications SDK.
|
|
3
|
+
*
|
|
4
|
+
* This is an example/template, NOT an upstream-owned integration: it implements
|
|
5
|
+
* the documented WS protocol (see `docs/notifications-sdk.md`) so you can copy it
|
|
6
|
+
* to build Discord/Slack/etc. clients with zero upstream changes. The Bot API
|
|
7
|
+
* transport shape is salvaged from the removed `telegram-remote` package.
|
|
8
|
+
*
|
|
9
|
+
* Flow: read the endpoint discovery file -> connect to the session WS -> render
|
|
10
|
+
* `action_needed` to a Telegram chat (inline keyboard for options) -> map button
|
|
11
|
+
* taps / text replies to `reply` frames -> reflect `action_resolved` /
|
|
12
|
+
* `reply_rejected`.
|
|
13
|
+
*
|
|
14
|
+
* Dependency-free: uses global `fetch` and `WebSocket` (Bun/Node 22+).
|
|
15
|
+
*/
|
|
16
|
+
/** One inline-keyboard button. */
|
|
17
|
+
export interface InlineButton {
|
|
18
|
+
text: string;
|
|
19
|
+
callback_data: string;
|
|
20
|
+
}
|
|
21
|
+
/** A rendered Telegram message for an `action_needed`. */
|
|
22
|
+
export interface RenderedMessage {
|
|
23
|
+
text: string;
|
|
24
|
+
inline_keyboard?: InlineButton[][];
|
|
25
|
+
}
|
|
26
|
+
/** Encode `actionId` + option `index` into Telegram callback_data (<=64 bytes). */
|
|
27
|
+
export declare function encodeCallbackData(actionId: string, index: number): string;
|
|
28
|
+
/** Decode callback_data produced by {@link encodeCallbackData}. */
|
|
29
|
+
export declare function decodeCallbackData(data: string): {
|
|
30
|
+
id: string;
|
|
31
|
+
index: number;
|
|
32
|
+
} | null;
|
|
33
|
+
export interface CallbackRoute {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
actionId: string;
|
|
36
|
+
answer: number | string;
|
|
37
|
+
}
|
|
38
|
+
export interface SerializedAliasTable {
|
|
39
|
+
version: 1;
|
|
40
|
+
next: number;
|
|
41
|
+
routes: Record<string, CallbackRoute>;
|
|
42
|
+
}
|
|
43
|
+
export interface AliasTable {
|
|
44
|
+
put(route: CallbackRoute): string;
|
|
45
|
+
get(alias: string): CallbackRoute | undefined;
|
|
46
|
+
delete(alias: string): boolean;
|
|
47
|
+
serialize(): SerializedAliasTable;
|
|
48
|
+
load(json: unknown): void;
|
|
49
|
+
entries(): Array<[string, CallbackRoute]>;
|
|
50
|
+
}
|
|
51
|
+
/** Create a compact, durable callback alias table. Serialized data contains routing ids only. */
|
|
52
|
+
export declare function createAliasTable(): AliasTable;
|
|
53
|
+
/** Render an `action_needed` payload into a Telegram message. */
|
|
54
|
+
export declare function buildActionMessage(action: {
|
|
55
|
+
kind: "ask" | "idle";
|
|
56
|
+
id: string;
|
|
57
|
+
question?: string;
|
|
58
|
+
options?: string[];
|
|
59
|
+
summary?: string;
|
|
60
|
+
}): RenderedMessage;
|
|
61
|
+
/** A protocol `reply` frame the client should send to the server. */
|
|
62
|
+
export interface ReplyFrame {
|
|
63
|
+
type: "reply";
|
|
64
|
+
id: string;
|
|
65
|
+
answer: number | string;
|
|
66
|
+
token: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Map a Telegram update into a reply frame, given the most recent pending ask id
|
|
70
|
+
* (for free-text replies). Returns `null` when the update is not actionable.
|
|
71
|
+
*/
|
|
72
|
+
export declare function telegramUpdateToReply(update: unknown, token: string, latestPendingAskId: string | undefined): ReplyFrame | null;
|
|
73
|
+
export type RouteDecision = ({
|
|
74
|
+
kind: "reply";
|
|
75
|
+
} & CallbackRoute) | {
|
|
76
|
+
kind: "stale";
|
|
77
|
+
reason: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "ignore";
|
|
80
|
+
};
|
|
81
|
+
export interface PendingAsk {
|
|
82
|
+
sessionId: string;
|
|
83
|
+
actionId: string;
|
|
84
|
+
}
|
|
85
|
+
export interface RouteInboundContext {
|
|
86
|
+
aliasTable: Pick<AliasTable, "get">;
|
|
87
|
+
messageRoutes: Map<string | number, CallbackRoute | Omit<CallbackRoute, "answer">>;
|
|
88
|
+
pendingBySession: (sessionId?: string) => PendingAsk[];
|
|
89
|
+
pairedChatId: string;
|
|
90
|
+
}
|
|
91
|
+
/** Route a Telegram update to a session/action without I/O. Fail closed under ambiguity. */
|
|
92
|
+
export declare function routeInboundUpdate(update: unknown, ctx: RouteInboundContext): RouteDecision;
|
|
93
|
+
/** Read `{url, token}` from an endpoint discovery file. */
|
|
94
|
+
export declare function readEndpoint(path: string): {
|
|
95
|
+
url: string;
|
|
96
|
+
token: string;
|
|
97
|
+
};
|
|
98
|
+
/** Options for {@link runTelegramReferenceClient}. */
|
|
99
|
+
export interface TelegramReferenceOptions {
|
|
100
|
+
botToken: string;
|
|
101
|
+
chatId: string;
|
|
102
|
+
endpointFile: string;
|
|
103
|
+
apiBase?: string;
|
|
104
|
+
fetchImpl?: typeof fetch;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Run the reference bridge until the WebSocket closes. Sends `action_needed` to
|
|
108
|
+
* the chat and forwards taps/text as replies. This is a minimal example loop;
|
|
109
|
+
* production clients add reconnection, multi-chat routing, and persistence.
|
|
110
|
+
*/
|
|
111
|
+
export declare function runTelegramReferenceClient(opts: TelegramReferenceOptions): Promise<void>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed routing for inbound Telegram updates in threaded session mode.
|
|
3
|
+
*
|
|
4
|
+
* In the threaded surface, a free-text reply inside a session's forum topic
|
|
5
|
+
* injects a new user turn into that session (steering it at any time). That is
|
|
6
|
+
* remote control of the agent, so every inbound path must fail closed:
|
|
7
|
+
*
|
|
8
|
+
* - the update must come from the single paired chat id;
|
|
9
|
+
* - it must carry a `message_thread_id` (topic) that maps to a KNOWN session;
|
|
10
|
+
* - its `update_id` must not have been seen before (idempotency / replay guard);
|
|
11
|
+
* - the text must be non-empty.
|
|
12
|
+
*
|
|
13
|
+
* Anything ambiguous or unmapped is ignored with a reason rather than guessed.
|
|
14
|
+
* This module is pure (the dedupe set and topic map are injected) so the
|
|
15
|
+
* security rules are exhaustively unit-testable without a live Bot API.
|
|
16
|
+
*/
|
|
17
|
+
/** Minimal shape of the inbound Telegram message we route on. */
|
|
18
|
+
export interface InboundUpdate {
|
|
19
|
+
update_id?: unknown;
|
|
20
|
+
message?: {
|
|
21
|
+
message_id?: unknown;
|
|
22
|
+
text?: unknown;
|
|
23
|
+
caption?: unknown;
|
|
24
|
+
photo?: unknown;
|
|
25
|
+
document?: unknown;
|
|
26
|
+
video?: unknown;
|
|
27
|
+
audio?: unknown;
|
|
28
|
+
voice?: unknown;
|
|
29
|
+
animation?: unknown;
|
|
30
|
+
chat?: {
|
|
31
|
+
id?: unknown;
|
|
32
|
+
};
|
|
33
|
+
message_thread_id?: unknown;
|
|
34
|
+
};
|
|
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
|
+
}
|
|
47
|
+
/** Context for {@link decideThreadedInbound}. All lookups are injected. */
|
|
48
|
+
export interface ThreadedInboundCtx {
|
|
49
|
+
/** The single paired chat id (string-compared). */
|
|
50
|
+
pairedChatId: string;
|
|
51
|
+
/** Resolve a topic/thread id to its owning session id, or undefined. */
|
|
52
|
+
topicToSession: (threadId: string) => string | undefined;
|
|
53
|
+
/** Whether this `update_id` has already been processed. */
|
|
54
|
+
isDuplicate: (updateId: number) => boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Outcome of routing an inbound update. */
|
|
57
|
+
export type ThreadedInboundDecision = {
|
|
58
|
+
kind: "inject";
|
|
59
|
+
sessionId: string;
|
|
60
|
+
text: string;
|
|
61
|
+
updateId: number;
|
|
62
|
+
threadId: string;
|
|
63
|
+
messageId?: number;
|
|
64
|
+
attachment?: InboundAttachment;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "duplicate";
|
|
67
|
+
updateId: number;
|
|
68
|
+
} | {
|
|
69
|
+
kind: "ignore";
|
|
70
|
+
reason: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Decide whether an inbound update should inject a user turn. Fail-closed:
|
|
74
|
+
* returns `ignore` (with a reason) or `duplicate` for anything that is not an
|
|
75
|
+
* unambiguous, first-seen, paired-chat, known-topic text message.
|
|
76
|
+
*/
|
|
77
|
+
export declare function decideThreadedInbound(update: InboundUpdate, ctx: ThreadedInboundCtx): ThreadedInboundDecision;
|