@gajae-code/coding-agent 0.8.2 → 0.9.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 +53 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +22 -0
- package/dist/types/notifications/index.d.ts +2 -0
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +30 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +8 -11
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +11 -9
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +76 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/launch-tmux.ts +6 -0
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +175 -19
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +18 -9
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +13 -18
- package/src/modes/controllers/selector-controller.ts +41 -0
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +43 -4
- package/src/notifications/index.ts +105 -9
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-cli.ts +54 -7
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +390 -56
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +68 -30
- package/src/session/agent-session.ts +734 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +143 -1
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
|
@@ -8,8 +8,8 @@ import { withFileLock } from "../config/file-lock";
|
|
|
8
8
|
import type { Settings } from "../config/settings";
|
|
9
9
|
import type { DaemonRuntimeInfo } from "../daemon/control-types";
|
|
10
10
|
import { resolveGjcRuntimeSpawnInfo } from "../daemon/runtime";
|
|
11
|
-
import { getNotificationConfig,
|
|
12
|
-
import { parseInThreadConfigCommand } from "./config-commands";
|
|
11
|
+
import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
|
|
12
|
+
import { parseInThreadConfigCommand, parseRichToggleCommand } from "./config-commands";
|
|
13
13
|
import { daemonPaths } from "./daemon-paths";
|
|
14
14
|
import {
|
|
15
15
|
buildCompactChoiceGrid,
|
|
@@ -44,8 +44,12 @@ import {
|
|
|
44
44
|
import { NotificationOperatorRuntime, OperatorBackoffPolicy, OperatorEventRouter } from "./operator-runtime";
|
|
45
45
|
import { RateLimitPool } from "./rate-limit-pool";
|
|
46
46
|
import { listRecentSessions } from "./recent-activity";
|
|
47
|
+
import { ReplySentStore } from "./reply-sent-store";
|
|
48
|
+
import { DraftStreamState, deliverDraft, shouldStreamDraft } from "./rich-draft";
|
|
49
|
+
import { deliverRichActionWithFallback, deliverRichWithFallback, shouldPromoteRich } from "./rich-render";
|
|
47
50
|
import {
|
|
48
51
|
type AliasTable,
|
|
52
|
+
buildActionMarkdown,
|
|
49
53
|
buildActionMessage,
|
|
50
54
|
type CallbackRoute,
|
|
51
55
|
createAliasTable,
|
|
@@ -636,7 +640,7 @@ export async function ensureTelegramDaemonRunning(
|
|
|
636
640
|
deps: TelegramDaemonDeps = {},
|
|
637
641
|
): Promise<EnsureDaemonResult> {
|
|
638
642
|
const cfg = getNotificationConfig(input.settings);
|
|
639
|
-
if (!
|
|
643
|
+
if (!isTelegramConfigured(cfg)) return "disabled";
|
|
640
644
|
const root = notificationRootForCwd(input.cwd);
|
|
641
645
|
const fp = tokenFingerprint(cfg.botToken);
|
|
642
646
|
const spawned = await spawnTelegramDaemonOwner(
|
|
@@ -843,6 +847,10 @@ export interface TelegramDaemonOptions {
|
|
|
843
847
|
* default applies (e.g. lifecycle control disabled), no control server starts.
|
|
844
848
|
*/
|
|
845
849
|
createLifecycleControlServer?: LifecycleControlServerFactory | null;
|
|
850
|
+
/** Rich text promotion (enabled by default; see rich-render.ts). */
|
|
851
|
+
rich?: { enabled: boolean };
|
|
852
|
+
/** Opt-in rich-draft streaming of live turn previews (off by default; see rich-draft.ts). */
|
|
853
|
+
richDraft?: { enabled: boolean };
|
|
846
854
|
}
|
|
847
855
|
|
|
848
856
|
interface SessionSocket {
|
|
@@ -869,6 +877,8 @@ interface PendingThreadedFrame {
|
|
|
869
877
|
export class TelegramNotificationDaemon {
|
|
870
878
|
readonly aliasTable: AliasTable;
|
|
871
879
|
readonly messageRoutes = new Map<string | number, CallbackRoute | Omit<CallbackRoute, "answer">>();
|
|
880
|
+
/** Telegram message id backing each streamed `${sessionId}:${coalesceKey}`, for in-place edits. */
|
|
881
|
+
private readonly liveMessages = new Map<string, number>();
|
|
872
882
|
readonly sessions = new Map<string, SessionSocket>();
|
|
873
883
|
private readonly runtime: NotificationOperatorRuntime;
|
|
874
884
|
private readonly sessionRouter: OperatorEventRouter<SessionSocket>;
|
|
@@ -881,6 +891,10 @@ export class TelegramNotificationDaemon {
|
|
|
881
891
|
private readonly pool: RateLimitPool<{ send: ThreadedSend; topicId?: string }>;
|
|
882
892
|
private readonly poller: TelegramUpdatePoller;
|
|
883
893
|
private readonly dispatchState = new TelegramEventDispatchState();
|
|
894
|
+
/** Original markdown of rich messages we sent (chat+message_id), for restoring reply context on inbound replies. */
|
|
895
|
+
private readonly replyStore: ReplySentStore;
|
|
896
|
+
/** Per-session debounce + monotonic draft-id state for opt-in draft streaming. */
|
|
897
|
+
private readonly draftStream = new DraftStreamState();
|
|
884
898
|
/** Identity-bearing sessions by repo/branch surface, used to avoid transient duplicate topics. */
|
|
885
899
|
private readonly topicOwnerByIdentity = new Map<string, string>();
|
|
886
900
|
/** Non-identity frames held until identity creates the correct thread. */
|
|
@@ -1118,7 +1132,7 @@ export class TelegramNotificationDaemon {
|
|
|
1118
1132
|
/** Build an authenticated lifecycle frame from a parsed command + identity. */
|
|
1119
1133
|
private buildLifecycleFrame(
|
|
1120
1134
|
parsed:
|
|
1121
|
-
| { kind: "create"; target: SessionCreateTarget }
|
|
1135
|
+
| { kind: "create"; target: SessionCreateTarget; modelPreset?: string }
|
|
1122
1136
|
| { kind: "close"; target: SessionCloseTarget }
|
|
1123
1137
|
| { kind: "resume"; target: SessionResumeTarget },
|
|
1124
1138
|
updateId: number,
|
|
@@ -1136,6 +1150,7 @@ export class TelegramNotificationDaemon {
|
|
|
1136
1150
|
chatId,
|
|
1137
1151
|
token,
|
|
1138
1152
|
target: parsed.target,
|
|
1153
|
+
modelPreset: parsed.modelPreset,
|
|
1139
1154
|
};
|
|
1140
1155
|
}
|
|
1141
1156
|
if (parsed.kind === "close") {
|
|
@@ -1240,6 +1255,7 @@ export class TelegramNotificationDaemon {
|
|
|
1240
1255
|
|
|
1241
1256
|
constructor(private readonly opts: TelegramDaemonOptions) {
|
|
1242
1257
|
this.fsImpl = opts.fs ?? nodeFs;
|
|
1258
|
+
this.replyStore = new ReplySentStore({ agentDir: opts.settings.getAgentDir(), fs: opts.fs });
|
|
1243
1259
|
this.aliasTable = createAliasTable();
|
|
1244
1260
|
this.botApi =
|
|
1245
1261
|
opts.botApi ??
|
|
@@ -1666,6 +1682,9 @@ export class TelegramNotificationDaemon {
|
|
|
1666
1682
|
})) as { ok?: boolean };
|
|
1667
1683
|
if (res?.ok === false) return;
|
|
1668
1684
|
this.topics.delete(sessionId);
|
|
1685
|
+
for (const k of [...this.liveMessages.keys()]) {
|
|
1686
|
+
if (k.startsWith(`${sessionId}:`)) this.liveMessages.delete(k);
|
|
1687
|
+
}
|
|
1669
1688
|
this.topicOwnerByIdentity.forEach((ownerSessionId, identityKey) => {
|
|
1670
1689
|
if (ownerSessionId === sessionId) this.topicOwnerByIdentity.delete(identityKey);
|
|
1671
1690
|
});
|
|
@@ -1793,14 +1812,81 @@ export class TelegramNotificationDaemon {
|
|
|
1793
1812
|
return { images, fileNotes };
|
|
1794
1813
|
}
|
|
1795
1814
|
|
|
1815
|
+
/**
|
|
1816
|
+
* Serialize all pool flushes. Every caller (`submitThreadedFrame`, the flat
|
|
1817
|
+
* fallback, the drain timer's `void this.flushPool()`, topic teardown) goes
|
|
1818
|
+
* through one promise chain, so two flushes never interleave — a live send can
|
|
1819
|
+
* never be in-flight while a finalized flush reads `liveMessages` and decides
|
|
1820
|
+
* to post a fresh (duplicate) final. Errors are swallowed so one failed flush
|
|
1821
|
+
* never poisons the queue (each flush is already best-effort internally).
|
|
1822
|
+
*/
|
|
1823
|
+
private flushChain: Promise<void> = Promise.resolve();
|
|
1824
|
+
private flushPool(): Promise<void> {
|
|
1825
|
+
const next = this.flushChain.then(() => this.flushPoolInner());
|
|
1826
|
+
this.flushChain = next.catch(() => {});
|
|
1827
|
+
return next;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1796
1830
|
/** Drain the shared rate-limit pool and deliver each granted send to its topic. */
|
|
1797
|
-
private async
|
|
1798
|
-
|
|
1831
|
+
private async flushPoolInner(): Promise<void> {
|
|
1832
|
+
const batch = this.pool.drain();
|
|
1833
|
+
// Within a batch a finalized frame supersedes any still-queued live frame for
|
|
1834
|
+
// the same streamed message (finalized outranks live), so drop the stale live
|
|
1835
|
+
// edit — otherwise the authoritative final text could be overwritten by an
|
|
1836
|
+
// older partial delivered right after it.
|
|
1837
|
+
const finalizedKeys = new Set<string>();
|
|
1838
|
+
for (const item of batch) {
|
|
1839
|
+
if (item.lane === "finalized" && item.coalesceKey !== undefined) {
|
|
1840
|
+
finalizedKeys.add(`${item.sessionId}:${item.coalesceKey}`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
// Cross-batch protection: also purge any live frame still QUEUED for a
|
|
1844
|
+
// message whose finalized frame is in this batch, so a stale live edit can
|
|
1845
|
+
// never be delivered on a later drain after the authoritative final.
|
|
1846
|
+
if (finalizedKeys.size > 0) {
|
|
1847
|
+
this.pool.removeWhere(
|
|
1848
|
+
it =>
|
|
1849
|
+
it.lane === "live" &&
|
|
1850
|
+
it.coalesceKey !== undefined &&
|
|
1851
|
+
finalizedKeys.has(`${it.sessionId}:${it.coalesceKey}`),
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
for (const item of batch) {
|
|
1799
1855
|
const { send, topicId } = item.payload;
|
|
1800
1856
|
if (topicId && !(await this.pairedChatIsPrivate())) continue;
|
|
1801
1857
|
// Threaded topic when available; otherwise deliver flat to the paired chat.
|
|
1802
1858
|
const threadField = topicId ? { message_thread_id: Number(topicId) } : {};
|
|
1859
|
+
const ckey = send.editable ? item.coalesceKey : undefined;
|
|
1860
|
+
const editKey = ckey !== undefined ? `${item.sessionId}:${ckey}` : undefined;
|
|
1861
|
+
if (item.lane === "live" && editKey && finalizedKeys.has(editKey)) continue;
|
|
1803
1862
|
try {
|
|
1863
|
+
// Draft streaming (opt-in, off by default): stream a live turn frame as a
|
|
1864
|
+
// best-effort rich-draft preview, debounced to >=1.5s per session through
|
|
1865
|
+
// this same rate-limited drain; a finalized frame ends the turn's draft
|
|
1866
|
+
// window. Entirely inert when richDraft is off (the enabled gate /
|
|
1867
|
+
// shouldStreamDraft fail closed), so off-state HTML request bodies stay
|
|
1868
|
+
// byte-identical.
|
|
1869
|
+
if (this.opts.richDraft?.enabled === true && this.opts.rich?.enabled !== false) {
|
|
1870
|
+
if (send.lane === "finalized" && send.method === "sendMessage") {
|
|
1871
|
+
this.draftStream.reset(item.sessionId);
|
|
1872
|
+
} else if (
|
|
1873
|
+
shouldStreamDraft({
|
|
1874
|
+
enabled: this.opts.richDraft.enabled,
|
|
1875
|
+
send,
|
|
1876
|
+
})
|
|
1877
|
+
) {
|
|
1878
|
+
const draftId = this.draftStream.tryClaim(item.sessionId, this.opts.now?.() ?? Date.now());
|
|
1879
|
+
if (draftId !== undefined) {
|
|
1880
|
+
await deliverDraft(
|
|
1881
|
+
this.botApi,
|
|
1882
|
+
{ chat_id: this.opts.chatId, ...threadField },
|
|
1883
|
+
draftId,
|
|
1884
|
+
send.richDraftMarkdown!,
|
|
1885
|
+
logger,
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1804
1890
|
if (send.method === "sendPhoto" && send.photoBase64) {
|
|
1805
1891
|
// Real photo upload (the default botApi multiparts base64 -> file).
|
|
1806
1892
|
await this.botApi.call("sendPhoto", {
|
|
@@ -1822,21 +1908,165 @@ export class TelegramNotificationDaemon {
|
|
|
1822
1908
|
parse_mode: TELEGRAM_PARSE_MODE,
|
|
1823
1909
|
});
|
|
1824
1910
|
} else if (send.text) {
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1911
|
+
// Rich pre-branch: promote stable non-editable finalized text to a fresh
|
|
1912
|
+
// sendRichMessage when enabled. Off/miss falls through to the unchanged
|
|
1913
|
+
// upstream edit/send path, so off behavior is byte-identical.
|
|
1914
|
+
if (
|
|
1915
|
+
shouldPromoteRich({
|
|
1916
|
+
enabled: this.opts.rich?.enabled === false ? false : true,
|
|
1917
|
+
send,
|
|
1918
|
+
})
|
|
1919
|
+
) {
|
|
1920
|
+
const sendHtmlFallback = async () => {
|
|
1921
|
+
// Fairness: this frame consumed exactly one token, so send only the
|
|
1922
|
+
// first HTML chunk now and requeue any continuations as their own
|
|
1923
|
+
// non-editable, HTML-only pool items (rich markers stripped) — same
|
|
1924
|
+
// per-token discipline as the non-rich split path.
|
|
1925
|
+
const chunks = splitTelegramHtml(send.text!);
|
|
1926
|
+
await this.botApi.call("sendMessage", {
|
|
1927
|
+
chat_id: this.opts.chatId,
|
|
1928
|
+
...threadField,
|
|
1929
|
+
text: chunks[0]!,
|
|
1930
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
1931
|
+
});
|
|
1932
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
1933
|
+
this.pool.submit({
|
|
1934
|
+
sessionId: item.sessionId,
|
|
1935
|
+
lane: item.lane,
|
|
1936
|
+
payload: {
|
|
1937
|
+
send: {
|
|
1938
|
+
...send,
|
|
1939
|
+
method: "sendMessage",
|
|
1940
|
+
text: chunks[i]!,
|
|
1941
|
+
editable: false,
|
|
1942
|
+
coalesceKey: undefined,
|
|
1943
|
+
photoBase64: undefined,
|
|
1944
|
+
documentBase64: undefined,
|
|
1945
|
+
richMarkdown: undefined,
|
|
1946
|
+
richDraftMarkdown: undefined,
|
|
1947
|
+
richClass: undefined,
|
|
1948
|
+
},
|
|
1949
|
+
topicId,
|
|
1950
|
+
},
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
const richMessageId = await deliverRichWithFallback(
|
|
1955
|
+
this.botApi,
|
|
1956
|
+
{ chat_id: this.opts.chatId, ...threadField },
|
|
1957
|
+
send,
|
|
1958
|
+
sendHtmlFallback,
|
|
1959
|
+
logger,
|
|
1960
|
+
);
|
|
1961
|
+
// Index the sent rich message so an inbound reply to it can restore
|
|
1962
|
+
// the original markdown as context (Telegram does not echo it back).
|
|
1963
|
+
if (richMessageId !== undefined) {
|
|
1964
|
+
await this.replyStore.record({
|
|
1965
|
+
chatId: this.opts.chatId,
|
|
1966
|
+
messageId: richMessageId,
|
|
1967
|
+
text: send.richMarkdown!,
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
} else {
|
|
1971
|
+
const chunks = splitTelegramHtml(send.text);
|
|
1972
|
+
const existingId = editKey ? this.liveMessages.get(editKey) : undefined;
|
|
1973
|
+
let firstMessageId: number | undefined;
|
|
1974
|
+
if (editKey && existingId !== undefined) {
|
|
1975
|
+
// Edit the existing streamed message in place with the first chunk
|
|
1976
|
+
// so a finalized turn never leaves a stale live preview. A LOCAL
|
|
1977
|
+
// try/catch keeps a failed edit from aborting the continuation
|
|
1978
|
+
// requeue below; "message is not modified" is a success (the message
|
|
1979
|
+
// already shows this text); a missing/deleted backing message (or a
|
|
1980
|
+
// transport error) resends so the first chunk is never lost.
|
|
1981
|
+
let edited = false;
|
|
1982
|
+
try {
|
|
1983
|
+
const res = (await this.botApi.call("editMessageText", {
|
|
1984
|
+
chat_id: this.opts.chatId,
|
|
1985
|
+
message_id: existingId,
|
|
1986
|
+
text: chunks[0],
|
|
1987
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
1988
|
+
})) as { ok?: boolean; description?: string } | null;
|
|
1989
|
+
edited = res?.ok !== false || /not modified/i.test(String(res?.description ?? ""));
|
|
1990
|
+
} catch {
|
|
1991
|
+
edited = false;
|
|
1992
|
+
}
|
|
1993
|
+
if (edited) {
|
|
1994
|
+
firstMessageId = existingId;
|
|
1995
|
+
} else {
|
|
1996
|
+
const res = (await this.botApi.call("sendMessage", {
|
|
1997
|
+
chat_id: this.opts.chatId,
|
|
1998
|
+
...threadField,
|
|
1999
|
+
text: chunks[0]!,
|
|
2000
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
2001
|
+
})) as { result?: { message_id?: number } };
|
|
2002
|
+
firstMessageId = res?.result?.message_id;
|
|
2003
|
+
}
|
|
2004
|
+
} else {
|
|
2005
|
+
// No streamed message to edit: a single granted slot maps to a
|
|
2006
|
+
// single Telegram send.
|
|
2007
|
+
const res = (await this.botApi.call("sendMessage", {
|
|
2008
|
+
chat_id: this.opts.chatId,
|
|
2009
|
+
...threadField,
|
|
2010
|
+
text: chunks[0]!,
|
|
2011
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
2012
|
+
})) as { result?: { message_id?: number } };
|
|
2013
|
+
firstMessageId = res?.result?.message_id;
|
|
2014
|
+
}
|
|
2015
|
+
// Continuation chunks are FINALIZED-lane only. A live preview is a
|
|
2016
|
+
// single edit-safe chunk (its authoritative full text arrives with the
|
|
2017
|
+
// finalized frame), so a split live frame never fans out into stale,
|
|
2018
|
+
// non-coalesced continuation messages. Finalized continuations are
|
|
2019
|
+
// fresh, non-editable, HTML-only sends (rich markers stripped) so they
|
|
2020
|
+
// can never be re-promoted to a duplicate sendRichMessage.
|
|
2021
|
+
if (item.lane !== "live") {
|
|
2022
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
2023
|
+
this.pool.submit({
|
|
2024
|
+
sessionId: item.sessionId,
|
|
2025
|
+
lane: item.lane,
|
|
2026
|
+
payload: {
|
|
2027
|
+
send: {
|
|
2028
|
+
...send,
|
|
2029
|
+
method: "sendMessage",
|
|
2030
|
+
text: chunks[i]!,
|
|
2031
|
+
editable: false,
|
|
2032
|
+
coalesceKey: undefined,
|
|
2033
|
+
photoBase64: undefined,
|
|
2034
|
+
documentBase64: undefined,
|
|
2035
|
+
richMarkdown: undefined,
|
|
2036
|
+
richDraftMarkdown: undefined,
|
|
2037
|
+
richClass: undefined,
|
|
2038
|
+
},
|
|
2039
|
+
topicId,
|
|
2040
|
+
},
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (editKey && ckey !== undefined && firstMessageId !== undefined) {
|
|
2045
|
+
this.recordLiveMessage(item.sessionId, ckey, firstMessageId);
|
|
2046
|
+
}
|
|
1832
2047
|
}
|
|
1833
2048
|
}
|
|
1834
2049
|
} catch {
|
|
1835
|
-
// Best-effort: a failed send must never stop the daemon.
|
|
2050
|
+
// Best-effort: a failed send/edit must never stop the daemon.
|
|
1836
2051
|
}
|
|
1837
2052
|
}
|
|
1838
2053
|
}
|
|
1839
2054
|
|
|
2055
|
+
/**
|
|
2056
|
+
* Track the Telegram message id backing a streamed `(sessionId, coalesceKey)`
|
|
2057
|
+
* so later live/finalized frames edit it in place. Evicts this session's stale
|
|
2058
|
+
* same-category entries (e.g. prior turns) so the map stays bounded.
|
|
2059
|
+
*/
|
|
2060
|
+
private recordLiveMessage(sessionId: string, coalesceKey: string, messageId: number): void {
|
|
2061
|
+
const mapKey = `${sessionId}:${coalesceKey}`;
|
|
2062
|
+
const category = coalesceKey.split(":")[0] ?? "";
|
|
2063
|
+
const prefix = `${sessionId}:${category}:`;
|
|
2064
|
+
for (const k of [...this.liveMessages.keys()]) {
|
|
2065
|
+
if (k !== mapKey && k.startsWith(prefix)) this.liveMessages.delete(k);
|
|
2066
|
+
}
|
|
2067
|
+
this.liveMessages.set(mapKey, messageId);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1840
2070
|
/**
|
|
1841
2071
|
* Threaded Mode is unavailable (the bot owner has not enabled forum topics in
|
|
1842
2072
|
* @BotFather, so `createForumTopic` fails). Deliver the rendered frame flat to
|
|
@@ -1882,7 +2112,7 @@ export class TelegramNotificationDaemon {
|
|
|
1882
2112
|
try {
|
|
1883
2113
|
await this.botApi.call("sendMessage", {
|
|
1884
2114
|
chat_id: this.opts.chatId,
|
|
1885
|
-
text: "
|
|
2115
|
+
text: "Flat Telegram private chat supports outbound notifications and inline ask buttons only. Enable Threaded Mode in @BotFather > Bot Settings > Threads Settings for free-text replies and session commands.",
|
|
1886
2116
|
parse_mode: TELEGRAM_PARSE_MODE,
|
|
1887
2117
|
});
|
|
1888
2118
|
} catch {
|
|
@@ -2031,24 +2261,58 @@ export class TelegramNotificationDaemon {
|
|
|
2031
2261
|
});
|
|
2032
2262
|
const options = Array.isArray(msg.options) ? msg.options : [];
|
|
2033
2263
|
// Daemon keyboards use alias callback data with compact one-based tap targets;
|
|
2034
|
-
// full option text is rendered in the message body by buildActionMessage.
|
|
2264
|
+
// full option text is rendered in the message body by buildActionMessage/buildActionMarkdown.
|
|
2035
2265
|
const inline_keyboard = buildCompactChoiceGrid(options, (i: number) =>
|
|
2036
2266
|
this.aliasTable.put({ sessionId: session.sessionId, actionId: msg.id, answer: i }),
|
|
2037
2267
|
);
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2268
|
+
// HTML delivery: one sendMessage per chunk, keyboard on the last chunk;
|
|
2269
|
+
// returns the last chunk's message_id (the reply-routable message).
|
|
2270
|
+
const sendHtmlChunks = async (): Promise<number | undefined> => {
|
|
2271
|
+
const chunks = splitTelegramHtml(rendered.text);
|
|
2272
|
+
let result: { result?: { message_id?: number } } = {};
|
|
2273
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2274
|
+
result = (await this.botApi.call("sendMessage", {
|
|
2275
|
+
chat_id: this.opts.chatId,
|
|
2276
|
+
...threadField,
|
|
2277
|
+
text: chunks[i]!,
|
|
2278
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
2279
|
+
...(i === chunks.length - 1 && inline_keyboard.length ? { reply_markup: { inline_keyboard } } : {}),
|
|
2280
|
+
})) as { result?: { message_id?: number } };
|
|
2281
|
+
}
|
|
2282
|
+
return result.result?.message_id;
|
|
2283
|
+
};
|
|
2284
|
+
const kind = msg.kind === "idle" ? "idle" : "ask";
|
|
2285
|
+
if (this.opts.rich?.enabled !== false) {
|
|
2286
|
+
// Rich (default on): promote to sendRichMessage with a top-level
|
|
2287
|
+
// reply_markup (probe-confirmed). Any miss falls back to the HTML loop.
|
|
2288
|
+
|
|
2289
|
+
const outcome = await deliverRichActionWithFallback(
|
|
2290
|
+
this.botApi,
|
|
2291
|
+
{ chat_id: this.opts.chatId, ...threadField },
|
|
2292
|
+
{
|
|
2293
|
+
markdown: buildActionMarkdown({
|
|
2294
|
+
kind,
|
|
2295
|
+
question: msg.question,
|
|
2296
|
+
options: msg.options,
|
|
2297
|
+
summary: msg.summary,
|
|
2298
|
+
}),
|
|
2299
|
+
replyMarkup: kind === "ask" && inline_keyboard.length ? { inline_keyboard } : undefined,
|
|
2300
|
+
requireMessageId: kind === "ask",
|
|
2301
|
+
},
|
|
2302
|
+
sendHtmlChunks,
|
|
2303
|
+
logger,
|
|
2304
|
+
);
|
|
2305
|
+
// Only asks are reply-routable; idle pings register no route.
|
|
2306
|
+
if (kind === "ask" && outcome.messageId !== undefined)
|
|
2307
|
+
this.messageRoutes.set(String(outcome.messageId), { sessionId: session.sessionId, actionId: msg.id });
|
|
2308
|
+
} else {
|
|
2309
|
+
// Off: byte-identical to the pre-rich HTML path.
|
|
2310
|
+
const messageId = await sendHtmlChunks();
|
|
2311
|
+
// Only asks are reply-routable; idle pings register no route (parity
|
|
2312
|
+
// with the rich branch and correct even in the byte-identical off path).
|
|
2313
|
+
if (kind === "ask" && messageId !== undefined)
|
|
2314
|
+
this.messageRoutes.set(String(messageId), { sessionId: session.sessionId, actionId: msg.id });
|
|
2048
2315
|
}
|
|
2049
|
-
const messageId = result.result?.message_id;
|
|
2050
|
-
if (messageId !== undefined)
|
|
2051
|
-
this.messageRoutes.set(String(messageId), { sessionId: session.sessionId, actionId: msg.id });
|
|
2052
2316
|
await this.persistAliases();
|
|
2053
2317
|
} else if (msg.type === "action_resolved" && msg.id) {
|
|
2054
2318
|
session.pending.delete(msg.id);
|
|
@@ -2104,6 +2368,65 @@ export class TelegramNotificationDaemon {
|
|
|
2104
2368
|
}
|
|
2105
2369
|
}
|
|
2106
2370
|
}
|
|
2371
|
+
// Rich-message toggle (/rich on|off): daemon-local delivery policy, NOT a
|
|
2372
|
+
// session config forward. Handled at paired-chat pre-routing, before threaded
|
|
2373
|
+
// injection and independent of any session WebSocket, so it works even when
|
|
2374
|
+
// no session is connected and never becomes an ask answer.
|
|
2375
|
+
{
|
|
2376
|
+
const m = (update as { update_id?: number; message?: Record<string, unknown> }).message;
|
|
2377
|
+
const chat = m?.chat as { id?: unknown } | undefined;
|
|
2378
|
+
const cmdText = typeof m?.text === "string" ? m.text : undefined;
|
|
2379
|
+
const rawFirst = cmdText?.trim().split(/\s+/)[0]?.toLowerCase();
|
|
2380
|
+
// Fail-closed: intercept ANY "/rich" or "/rich@<anything>" form (Telegram
|
|
2381
|
+
// appends @botname in groups; the bot username may be unknown if getMe
|
|
2382
|
+
// failed) so a rich command is never leaked into threaded injection / an
|
|
2383
|
+
// ask answer. Argument validity is decided by parseRichToggleCommand below.
|
|
2384
|
+
const isRichCommand = rawFirst?.split("@")[0] === "/rich";
|
|
2385
|
+
if (m !== undefined && String(chat?.id) === String(this.opts.chatId) && isRichCommand) {
|
|
2386
|
+
// Fail-closed: /rich mutates global config, so honor it ONLY in a PRIVATE
|
|
2387
|
+
// paired chat — the same contract as session delivery and lifecycle
|
|
2388
|
+
// commands. A group/supergroup chatId (legacy or hand-edited) must never
|
|
2389
|
+
// let an arbitrary chat member toggle the owner's notification config.
|
|
2390
|
+
if (!(await this.pairedChatIsPrivate())) return;
|
|
2391
|
+
const updateId = (update as { update_id?: number }).update_id;
|
|
2392
|
+
// Dedupe redelivered updates so a toggle+confirmation runs at most once.
|
|
2393
|
+
if (typeof updateId === "number") {
|
|
2394
|
+
if (this.dispatchState.seenUpdateIds.has(updateId)) return;
|
|
2395
|
+
await this.rememberSeenUpdateId(updateId);
|
|
2396
|
+
}
|
|
2397
|
+
const threadField =
|
|
2398
|
+
typeof m.message_thread_id === "number" ? { message_thread_id: m.message_thread_id as number } : {};
|
|
2399
|
+
const reply = async (body: string): Promise<void> => {
|
|
2400
|
+
try {
|
|
2401
|
+
await this.botApi.call("sendMessage", {
|
|
2402
|
+
chat_id: this.opts.chatId,
|
|
2403
|
+
...threadField,
|
|
2404
|
+
text: body,
|
|
2405
|
+
parse_mode: TELEGRAM_PARSE_MODE,
|
|
2406
|
+
});
|
|
2407
|
+
} catch {
|
|
2408
|
+
// Best-effort confirmation; never block on the notice.
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
const desired = parseRichToggleCommand(cmdText ?? "");
|
|
2412
|
+
if (desired === undefined) {
|
|
2413
|
+
await reply("Usage: /rich on|off");
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
try {
|
|
2417
|
+
await this.opts.settings.set("notifications.telegram.rich.enabled", desired);
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
logger.warn(
|
|
2420
|
+
`notifications: /rich settings write failed (${err instanceof Error ? err.message : String(err)}); runtime unchanged`,
|
|
2421
|
+
);
|
|
2422
|
+
await reply("Rich messages: unchanged (settings write failed)");
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
this.opts.rich = { enabled: desired };
|
|
2426
|
+
await reply(desired ? "Rich messages: on" : "Rich messages: off");
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2107
2430
|
// Threaded injection: a free-text message in a known topic (not a button
|
|
2108
2431
|
// tap and not a reply to a specific ask message) injects a user turn or an
|
|
2109
2432
|
// in-thread config command. Fail-closed: paired chat + known topic +
|
|
@@ -2134,7 +2457,17 @@ export class TelegramNotificationDaemon {
|
|
|
2134
2457
|
const images = attachmentResult?.images ?? [];
|
|
2135
2458
|
const fileNotes = attachmentResult?.fileNotes ?? [];
|
|
2136
2459
|
const hasMedia = images.length > 0 || fileNotes.length > 0;
|
|
2137
|
-
const
|
|
2460
|
+
const baseInjectedText = [inbound.text, ...fileNotes].filter(Boolean).join("\n");
|
|
2461
|
+
// A reply to a rich message we sent (not an ask route) loses its original
|
|
2462
|
+
// text: Telegram does not echo it in reply_to_message. Restore it from the
|
|
2463
|
+
// reply index as a labeled context prefix; a miss leaves the turn unchanged.
|
|
2464
|
+
const repliedOriginal =
|
|
2465
|
+
typeof replyTo === "number"
|
|
2466
|
+
? this.replyStore.lookup({ chatId: this.opts.chatId, messageId: replyTo })
|
|
2467
|
+
: undefined;
|
|
2468
|
+
const injectedText = repliedOriginal
|
|
2469
|
+
? `> replied-to message:\n${repliedOriginal}\n\n${baseInjectedText}`
|
|
2470
|
+
: baseInjectedText;
|
|
2138
2471
|
const cfg = hasMedia ? undefined : parseInThreadConfigCommand(inbound.text);
|
|
2139
2472
|
// A plain (non-config) message while an ask is pending for this session
|
|
2140
2473
|
// answers that ask as free-input — instead of starting a new user turn.
|
|
@@ -2214,7 +2547,8 @@ export class TelegramNotificationDaemon {
|
|
|
2214
2547
|
{ command: "verbose", description: "Mirror full tool output + reasoning in this thread" },
|
|
2215
2548
|
{ command: "lean", description: "Mirror assistant text + tool names only (default)" },
|
|
2216
2549
|
{ command: "redact", description: "Toggle redaction of streamed content: /redact <on|off>" },
|
|
2217
|
-
{ command: "
|
|
2550
|
+
{ command: "rich", description: "Toggle rich Telegram delivery: /rich <on|off>" },
|
|
2551
|
+
{ command: "session_create", description: "Create a GJC session: path, worktree, or dir [--mpreset]" },
|
|
2218
2552
|
{ command: "session_recent", description: "List recent GJC sessions" },
|
|
2219
2553
|
{ command: "session_close", description: "Close a GJC-managed session" },
|
|
2220
2554
|
{ command: "session_resume", description: "Resume or reattach a session" },
|
|
@@ -2244,6 +2578,7 @@ export class TelegramNotificationDaemon {
|
|
|
2244
2578
|
await this.loadAliases();
|
|
2245
2579
|
await this.loadTopics();
|
|
2246
2580
|
await this.loadSeenUpdateIds();
|
|
2581
|
+
await this.replyStore.load();
|
|
2247
2582
|
await this.runScan();
|
|
2248
2583
|
// Owner-only: start the session-lifecycle control server now that
|
|
2249
2584
|
// ownership is confirmed (singleton-safe). Best-effort; degrades.
|
|
@@ -2264,31 +2599,30 @@ export class TelegramNotificationDaemon {
|
|
|
2264
2599
|
await this.runScan();
|
|
2265
2600
|
if (await this.controlStopRequested()) break;
|
|
2266
2601
|
const idleElapsed = this.runtime.now() - idleSince >= (this.opts.idleTimeoutMs ?? 60_000);
|
|
2267
|
-
if (this.sessions.size
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
//
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
}
|
|
2602
|
+
if (this.sessions.size > 0) {
|
|
2603
|
+
idleSince = this.runtime.now();
|
|
2604
|
+
} else if (idleElapsed) {
|
|
2605
|
+
// Zero sessions past the idle window: exit so the owner does not run
|
|
2606
|
+
// forever. An active session resets the idle window above.
|
|
2607
|
+
break;
|
|
2608
|
+
}
|
|
2609
|
+
// Poll getUpdates whenever the daemon owns the token — even with zero
|
|
2610
|
+
// sessions and no lifecycle control — so daemon-local commands (/rich,
|
|
2611
|
+
// /session_*) are always received until idle-exit.
|
|
2612
|
+
const activePoll = this.runtime.createAbortController();
|
|
2613
|
+
try {
|
|
2614
|
+
await this.pollOnce(activePoll.signal);
|
|
2615
|
+
this.loopBackoff.reset();
|
|
2616
|
+
} catch (e) {
|
|
2617
|
+
// A transient getUpdates/network failure must not kill the daemon.
|
|
2618
|
+
// Back off (bounded, below the heartbeat TTL) and keep renewing
|
|
2619
|
+
// ownership at the loop top.
|
|
2620
|
+
const backoffMs = this.loopBackoff.next();
|
|
2621
|
+
logger.warn(`notifications: getUpdates failed, backing off ${backoffMs}ms: ${String(e)}`);
|
|
2622
|
+
await this.runtime.sleep(backoffMs);
|
|
2623
|
+
continue;
|
|
2624
|
+
} finally {
|
|
2625
|
+
this.runtime.clearAbortController(activePoll);
|
|
2292
2626
|
}
|
|
2293
2627
|
if (await this.controlStopRequested()) break;
|
|
2294
2628
|
await this.runtime.sleep(10);
|
|
@@ -143,6 +143,23 @@ export function buildActionMessage(action: {
|
|
|
143
143
|
return { text: body, inline_keyboard };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** Render an `action_needed` body as raw markdown (rich-message source; the HTML fallback stays on buildActionMessage). */
|
|
147
|
+
export function buildActionMarkdown(action: {
|
|
148
|
+
kind: "ask" | "idle";
|
|
149
|
+
question?: string;
|
|
150
|
+
options?: string[];
|
|
151
|
+
summary?: string;
|
|
152
|
+
}): string {
|
|
153
|
+
if (action.kind === "idle") {
|
|
154
|
+
return action.summary ? `🟢 Agent idle\n${action.summary}` : "🟢 Agent idle";
|
|
155
|
+
}
|
|
156
|
+
const heading = `❓ **${action.question ?? "Question"}**`;
|
|
157
|
+
const options = action.options ?? [];
|
|
158
|
+
if (options.length === 0) return `${heading}\n\n(reply with text)`;
|
|
159
|
+
const list = options.map((label, i) => `${i + 1}. ${label.replace(/^\s*\d+[.)]\s+/, "")}`).join("\n");
|
|
160
|
+
return `${heading}\n\n${list}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
/** Send Telegram HTML text chunks sequentially so long messages preserve order. */
|
|
147
164
|
export async function sendTelegramHtmlChunks(
|
|
148
165
|
send: TelegramSend,
|