@gajae-code/coding-agent 0.6.5 → 0.7.0

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 (127) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +23 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +38 -0
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
  19. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  23. package/dist/types/modes/interactive-mode.d.ts +1 -1
  24. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  25. package/dist/types/modes/types.d.ts +7 -1
  26. package/dist/types/notifications/config-commands.d.ts +26 -0
  27. package/dist/types/notifications/config.d.ts +61 -0
  28. package/dist/types/notifications/helpers.d.ts +55 -0
  29. package/dist/types/notifications/html-format.d.ts +62 -0
  30. package/dist/types/notifications/index.d.ts +28 -0
  31. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  32. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  33. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  34. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  35. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  36. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  37. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  38. package/dist/types/notifications/threaded-render.d.ts +66 -0
  39. package/dist/types/notifications/topic-registry.d.ts +67 -0
  40. package/dist/types/rlm/index.d.ts +12 -0
  41. package/dist/types/session/agent-session.d.ts +39 -2
  42. package/dist/types/session/auth-storage.d.ts +1 -1
  43. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  44. package/dist/types/setup/credential-import.d.ts +3 -0
  45. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  46. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  47. package/dist/types/tools/index.d.ts +18 -0
  48. package/dist/types/tools/subagent.d.ts +3 -0
  49. package/package.json +7 -7
  50. package/scripts/build-binary.ts +3 -0
  51. package/src/async/job-manager.ts +5 -1
  52. package/src/cli/daemon-cli.ts +122 -0
  53. package/src/cli/notify-cli.ts +274 -0
  54. package/src/cli/setup-cli.ts +173 -84
  55. package/src/cli.ts +2 -0
  56. package/src/commands/daemon.ts +47 -0
  57. package/src/commands/notify.ts +61 -0
  58. package/src/commands/setup.ts +11 -1
  59. package/src/config/model-profile-activation.ts +74 -5
  60. package/src/config/model-profiles.ts +7 -4
  61. package/src/config/model-registry.ts +6 -3
  62. package/src/config/models-config-schema.ts +1 -1
  63. package/src/config/settings-schema.ts +29 -0
  64. package/src/coordinator/contract.ts +3 -0
  65. package/src/coordinator-mcp/server.ts +270 -1
  66. package/src/daemon/builtin.ts +46 -0
  67. package/src/daemon/control-types.ts +65 -0
  68. package/src/daemon/runtime.ts +51 -0
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  70. package/src/extensibility/extensions/runner.ts +4 -0
  71. package/src/extensibility/extensions/types.ts +8 -0
  72. package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
  73. package/src/gjc-runtime/state-runtime.ts +18 -4
  74. package/src/gjc-runtime/state-writer.ts +8 -8
  75. package/src/gjc-runtime/ultragoal-guard.ts +57 -2
  76. package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
  77. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  78. package/src/gjc-runtime/workflow-manifest.ts +11 -1
  79. package/src/goals/tools/goal-tool.ts +11 -2
  80. package/src/internal-urls/docs-index.generated.ts +6 -5
  81. package/src/main.ts +30 -0
  82. package/src/modes/acp/acp-event-mapper.ts +1 -0
  83. package/src/modes/components/hook-editor.ts +7 -2
  84. package/src/modes/components/oauth-selector.ts +19 -0
  85. package/src/modes/controllers/event-controller.ts +20 -0
  86. package/src/modes/controllers/selector-controller.ts +80 -17
  87. package/src/modes/interactive-mode.ts +6 -2
  88. package/src/modes/runtime-init.ts +1 -0
  89. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  90. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  91. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  92. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  93. package/src/modes/types.ts +7 -1
  94. package/src/modes/utils/ui-helpers.ts +23 -0
  95. package/src/notifications/config-commands.ts +50 -0
  96. package/src/notifications/config.ts +107 -0
  97. package/src/notifications/helpers.ts +135 -0
  98. package/src/notifications/html-format.ts +389 -0
  99. package/src/notifications/index.ts +663 -0
  100. package/src/notifications/rate-limit-pool.ts +179 -0
  101. package/src/notifications/telegram-cli.ts +194 -0
  102. package/src/notifications/telegram-daemon-cli.ts +74 -0
  103. package/src/notifications/telegram-daemon-control.ts +370 -0
  104. package/src/notifications/telegram-daemon.ts +1370 -0
  105. package/src/notifications/telegram-reference.ts +335 -0
  106. package/src/notifications/threaded-inbound.ts +80 -0
  107. package/src/notifications/threaded-render.ts +155 -0
  108. package/src/notifications/topic-registry.ts +133 -0
  109. package/src/rlm/index.ts +19 -0
  110. package/src/sdk.ts +16 -0
  111. package/src/session/agent-session.ts +113 -3
  112. package/src/session/auth-storage.ts +3 -0
  113. package/src/session/session-dump-format.ts +43 -2
  114. package/src/session/session-manager.ts +39 -5
  115. package/src/setup/credential-auto-import.ts +258 -0
  116. package/src/setup/credential-import.ts +17 -0
  117. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  118. package/src/setup/host-plugin-setup.ts +142 -0
  119. package/src/slash-commands/builtin-registry.ts +4 -1
  120. package/src/task/executor.ts +5 -1
  121. package/src/tools/ask-answer-registry.ts +25 -0
  122. package/src/tools/ask.ts +74 -4
  123. package/src/tools/image-gen.ts +5 -8
  124. package/src/tools/index.ts +19 -0
  125. package/src/tools/inspect-image.ts +16 -11
  126. package/src/tools/subagent-render.ts +7 -0
  127. package/src/tools/subagent.ts +38 -7
@@ -0,0 +1,276 @@
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
+ private flushTimer;
210
+ private scanTimer;
211
+ private scanning;
212
+ private typingTimer;
213
+ /** Sessions whose agent loop is currently busy (drives the typing indicator). */
214
+ private readonly busy;
215
+ /** Inbound update id → originating Telegram message, for delivery reactions. */
216
+ private readonly inboundReactions;
217
+ /** AbortController for the in-flight long poll; aborted by requestStop() to wake the loop. */
218
+ private activePoll;
219
+ /** Set when a cooperative stop has been requested (signal or control request). */
220
+ private stopRequested;
221
+ /** Current bounded backoff after a Telegram getUpdates 409 conflict (0 when healthy). */
222
+ private pollConflictBackoffMs;
223
+ /**
224
+ * Cooperatively stop the daemon: set the stop flag and abort the in-flight
225
+ * long poll so the run loop wakes immediately instead of waiting out the
226
+ * ~25s getUpdates timeout. Safe to call from a signal handler.
227
+ */
228
+ requestStop(_reason?: "reload" | "stop" | "signal"): void;
229
+ constructor(opts: TelegramDaemonOptions);
230
+ loadAliases(): Promise<void>;
231
+ persistAliases(): Promise<void>;
232
+ scanRoots(): Promise<void>;
233
+ connectSession(sessionId: string, url: string, token: string): void;
234
+ /**
235
+ * Start ack-based liveness for a session whose server advertised the
236
+ * `client_ping_pong` capability. Each interval drops the session when no pong
237
+ * has arrived within the TTL (the half-open case the socket never signals via
238
+ * `close`), otherwise sends a fresh application-level ping. The timer is bound
239
+ * to this exact session object.
240
+ */
241
+ private startLiveness;
242
+ /**
243
+ * Idempotent, identity-guarded session teardown. Clears the liveness timer,
244
+ * removes the map entry only when it still points at this exact session object
245
+ * (so a delayed old close cannot delete a replacement), and best-effort closes
246
+ * the socket. `scanRoots()` then reconnects the session.
247
+ */
248
+ private dropSession;
249
+ private static readonly THREADED_FRAMES;
250
+ private topicNameFor;
251
+ private ensureTopic;
252
+ private persistTopics;
253
+ loadTopics(): Promise<void>;
254
+ private flushPool;
255
+ private startFlushTimer;
256
+ private stopFlushTimer;
257
+ private runScan;
258
+ private startScanTimer;
259
+ private stopScanTimer;
260
+ private sendTyping;
261
+ private setReaction;
262
+ private startTypingTimer;
263
+ private stopTypingTimer;
264
+ handleSessionMessage(session: SessionSocket, msg: any): Promise<void>;
265
+ pendingBySession: (sessionId?: string) => PendingAsk[];
266
+ private sendStaleGuidance;
267
+ handleTelegramUpdate(update: unknown): Promise<void>;
268
+ pollOnce(signal?: AbortSignal): Promise<number>;
269
+ /** Abortable sleep honoring the injected timer; resolves early on abort. */
270
+ private sleep;
271
+ /** Sync the bot's Telegram command menu to what the daemon actually handles. */
272
+ registerBotCommands(): Promise<void>;
273
+ run(): Promise<void>;
274
+ private controlStopRequested;
275
+ }
276
+ 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,58 @@
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
+ chat?: {
24
+ id?: unknown;
25
+ };
26
+ message_thread_id?: unknown;
27
+ };
28
+ }
29
+ /** Context for {@link decideThreadedInbound}. All lookups are injected. */
30
+ export interface ThreadedInboundCtx {
31
+ /** The single paired chat id (string-compared). */
32
+ pairedChatId: string;
33
+ /** Resolve a topic/thread id to its owning session id, or undefined. */
34
+ topicToSession: (threadId: string) => string | undefined;
35
+ /** Whether this `update_id` has already been processed. */
36
+ isDuplicate: (updateId: number) => boolean;
37
+ }
38
+ /** Outcome of routing an inbound update. */
39
+ export type ThreadedInboundDecision = {
40
+ kind: "inject";
41
+ sessionId: string;
42
+ text: string;
43
+ updateId: number;
44
+ threadId: string;
45
+ messageId?: number;
46
+ } | {
47
+ kind: "duplicate";
48
+ updateId: number;
49
+ } | {
50
+ kind: "ignore";
51
+ reason: string;
52
+ };
53
+ /**
54
+ * Decide whether an inbound update should inject a user turn. Fail-closed:
55
+ * returns `ignore` (with a reason) or `duplicate` for anything that is not an
56
+ * unambiguous, first-seen, paired-chat, known-topic text message.
57
+ */
58
+ export declare function decideThreadedInbound(update: InboundUpdate, ctx: ThreadedInboundCtx): ThreadedInboundDecision;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pure rendering of threaded-session frames into Telegram send specs.
3
+ *
4
+ * The daemon receives the additive `ServerMessage` frames (identity_header,
5
+ * context_update, turn_stream, image_attachment, config_update) over the session
6
+ * WS and must turn each into a Bot API call scoped to the session's forum topic
7
+ * (`message_thread_id`), throttled through the shared rate-limit pool. This
8
+ * module is the pure frame→send mapping (including the priority lane and live-
9
+ * edit coalesce key), so rendering is unit-testable without a live Bot API.
10
+ */
11
+ import type { RateLimitLane } from "./rate-limit-pool";
12
+ /** A Telegram send derived from a threaded frame (topic id is applied by the daemon). */
13
+ export interface ThreadedSend {
14
+ method: "sendMessage" | "sendPhoto";
15
+ /** Rate-limit lane for prioritisation/fairness. */
16
+ lane: RateLimitLane;
17
+ /** Message text (sendMessage) or photo caption (sendPhoto). */
18
+ text?: string;
19
+ /** Base64 image bytes for sendPhoto. */
20
+ photoBase64?: string;
21
+ /** Image MIME type for sendPhoto. */
22
+ mime?: string;
23
+ /** Coalesce key for live edits (same key collapses to the latest). */
24
+ coalesceKey?: string;
25
+ /** True for the one-time identity header (the daemon pins it once). */
26
+ identity?: boolean;
27
+ }
28
+ interface ThreadedFrame {
29
+ type?: unknown;
30
+ sessionId?: unknown;
31
+ repo?: unknown;
32
+ branch?: unknown;
33
+ machine?: unknown;
34
+ title?: unknown;
35
+ lastMessage?: unknown;
36
+ task?: unknown;
37
+ goal?: unknown;
38
+ tokenUsage?: unknown;
39
+ model?: unknown;
40
+ diff?: unknown;
41
+ phase?: unknown;
42
+ text?: unknown;
43
+ messageRef?: unknown;
44
+ source?: unknown;
45
+ data?: unknown;
46
+ mime?: unknown;
47
+ caption?: unknown;
48
+ verbosity?: unknown;
49
+ redact?: unknown;
50
+ }
51
+ /** Format the one-time identity header as pinned bullets. */
52
+ export declare function formatIdentityHeader(frame: {
53
+ repo?: unknown;
54
+ branch?: unknown;
55
+ machine?: unknown;
56
+ sessionId?: unknown;
57
+ title?: unknown;
58
+ }): string;
59
+ /** Format a streamed context update into a compact block (omitting empty fields). */
60
+ export declare function formatContextUpdate(frame: ThreadedFrame): string | undefined;
61
+ /**
62
+ * Map a threaded frame to a Telegram send spec, or `undefined` when there is
63
+ * nothing to send (e.g. an empty context update or an unknown frame type).
64
+ */
65
+ export declare function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefined;
66
+ export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Per-session forum-topic registry for the threaded session surface.
3
+ *
4
+ * Each GJC session owns exactly one Telegram forum topic in the paired private
5
+ * DM. The topic is created once (via `createForumTopic`) and REUSED on resume,
6
+ * keyed by session id, so a resumed session streams back into its existing
7
+ * thread/history. The registry also tracks whether the one-time identity header
8
+ * has already been pinned, so it is sent exactly once per topic even across
9
+ * reconnects.
10
+ *
11
+ * State is a plain serialisable map persisted beside the daemon state files;
12
+ * topic creation is injected so this module is pure and unit-testable without a
13
+ * live Bot API.
14
+ */
15
+ /** Persisted record for one session's topic. */
16
+ export interface TopicRecord {
17
+ /** Telegram forum topic id (message_thread_id). */
18
+ topicId: string;
19
+ /** Whether the one-time identity header has been sent/pinned. */
20
+ identitySent: boolean;
21
+ /** Creation timestamp (ms epoch). */
22
+ createdAt: number;
23
+ /** Last applied topic title (for rename detection). */
24
+ name?: string;
25
+ }
26
+ /** Serialisable shape persisted to disk. */
27
+ export interface TopicRegistryState {
28
+ /** sessionId -> record. */
29
+ topics: Record<string, TopicRecord>;
30
+ }
31
+ export declare function emptyTopicRegistryState(): TopicRegistryState;
32
+ /**
33
+ * In-memory registry over a serialisable state. Topic creation is injected via
34
+ * `getOrCreateTopic`'s `create` callback (the daemon supplies a real
35
+ * `createForumTopic` call); reuse-on-resume is automatic when a record exists.
36
+ */
37
+ export declare class TopicRegistry {
38
+ private readonly topics;
39
+ /** Maps topicId -> sessionId for fast inbound routing. */
40
+ private readonly byTopic;
41
+ /** In-flight create promises, keyed by session, to dedupe concurrent creates. */
42
+ private readonly inflight;
43
+ constructor(state?: TopicRegistryState);
44
+ /** Merge a serialized state into this registry, preserving all persisted fields. */
45
+ load(state: TopicRegistryState): void;
46
+ /** Resolve the owning session for a topic id (for fail-closed inbound routing). */
47
+ sessionForTopic(topicId: string): string | undefined;
48
+ /** The existing topic record for a session, if any. */
49
+ get(sessionId: string): TopicRecord | undefined;
50
+ /**
51
+ * Return the existing topic for `sessionId`, or create one via `create`
52
+ * (called only on first use). Reuse-on-resume: an existing record is
53
+ * returned without invoking `create`.
54
+ */
55
+ getOrCreateTopic(sessionId: string, create: () => Promise<string>, now?: () => number): Promise<TopicRecord>;
56
+ /** Mark the identity header as sent for a session. Idempotent. */
57
+ markIdentitySent(sessionId: string): void;
58
+ /** Whether the identity header still needs sending for this session. */
59
+ needsIdentity(sessionId: string): boolean;
60
+ /**
61
+ * Record the topic's applied title. Returns `true` when it changed (so the
62
+ * caller should `editForumTopic`), `false` when already current or unknown.
63
+ */
64
+ applyName(sessionId: string, name: string): boolean;
65
+ /** Serialise for atomic persistence beside the daemon state. */
66
+ serialize(): TopicRegistryState;
67
+ }
@@ -31,5 +31,17 @@ export declare function buildRlmGoalObjective(input: {
31
31
  }): string;
32
32
  export declare function isRlmAutonomousRun(parsed: Pick<Args, "print" | "mode" | "messages">, pipedStdin: boolean): boolean;
33
33
  export declare function prepareRlmLaunchMode(parsed: Args, pipedStdin: boolean): boolean;
34
+ /**
35
+ * RLM artifacts are scoped under a GJC session directory and resolving their
36
+ * paths is a *write* (it must pick a concrete session). When `gjc rlm` runs
37
+ * standalone — no parent agent, no `GJC_SESSION_ID` in the environment — there is
38
+ * no session to resolve and `resolveGjcSessionForWrite` throws
39
+ * `missing_for_write`. Establish a dedicated GJC session id in that case and pin
40
+ * it into the environment so artifact-path resolution, the per-session activity
41
+ * marker, and the child agent's workflow state all share one writable session.
42
+ *
43
+ * Returns the resolved (existing or freshly generated) GJC session id.
44
+ */
45
+ export declare function ensureRlmGjcSessionId(): string;
34
46
  export declare function runRlmCommand(argv: string[]): Promise<void>;
35
47
  export {};
@@ -71,7 +71,7 @@ import { type AgentRegistry } from "../registry/agent-registry";
71
71
  import { type DiscoverableMCPSearchIndex, type DiscoverableMCPTool } from "../runtime-mcp/discoverable-tool-metadata";
72
72
  import { type SecretObfuscator } from "../secrets/obfuscator";
73
73
  import { type DiscoverableTool, type DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
74
- import type { ToolSession } from "../tools";
74
+ import type { AskAnswerSource, ToolSession } from "../tools";
75
75
  import type { CheckpointState } from "../tools/checkpoint";
76
76
  import { type TodoItem, type TodoPhase } from "../tools/todo-write";
77
77
  import type { ClientBridge } from "./client-bridge";
@@ -128,6 +128,9 @@ export type AgentSessionEvent = AgentEvent | {
128
128
  } | {
129
129
  type: "irc_message";
130
130
  message: CustomMessage;
131
+ } | {
132
+ type: "subagent_steer_message";
133
+ message: CustomMessage;
131
134
  } | {
132
135
  type: "notice";
133
136
  level: "info" | "warning" | "error";
@@ -558,6 +561,7 @@ export declare class AgentSession {
558
561
  getGoalModeState(): GoalModeState | undefined;
559
562
  setGoalModeState(state: GoalModeState | undefined): void;
560
563
  getWorkflowGateEmitter(): WorkflowGateEmitter | undefined;
564
+ getAskAnswerSource(): AskAnswerSource | undefined;
561
565
  setWorkflowGateEmitter(emitter: WorkflowGateEmitter | undefined): void;
562
566
  get goalRuntime(): GoalRuntime;
563
567
  markPlanReferenceSent(): void;
@@ -713,12 +717,38 @@ export declare class AgentSession {
713
717
  }): Promise<void>;
714
718
  setActiveModelProfile(name: string | undefined): void;
715
719
  getActiveModelProfile(): string | undefined;
720
+ /**
721
+ * The model selector ("provider/id") that resume restores as the session
722
+ * default — the latest session-log `model_change` with role="default".
723
+ * Model-profile activation snapshots this before mutating the session so a
724
+ * failed-activation rollback can restore the pre-activation resume default
725
+ * instead of promoting a transient runtime model to the resume default.
726
+ */
727
+ getSessionDefaultModelSelector(): string | undefined;
728
+ /**
729
+ * Re-assert the session resume default ("provider/id") in the session log
730
+ * WITHOUT touching the live runtime model. Appends a `model_change` with
731
+ * role="default"; never writes to global settings (apply-for-this-session
732
+ * semantics). Used by model-profile activation rollback to neutralize the
733
+ * profile main model the failed activation already recorded as the default.
734
+ */
735
+ recordResumeDefaultModel(selector: string): void;
716
736
  /**
717
737
  * Set model temporarily (for this session only).
718
738
  * Validates API key, saves to session log but NOT to settings.
739
+ *
740
+ * The change is recorded in the session log as `role: "temporary"` by
741
+ * default, which means it is NOT restored as the session default on resume —
742
+ * transient retry/fallback/context-promotion/plan switches must not clobber
743
+ * the user's explicit pick (issue #849). Model-profile activation passes
744
+ * `persistAsSessionDefault: true` so the profile's main model becomes the
745
+ * session default and survives resume, while still not being written to
746
+ * global settings (new sessions keep the global default).
719
747
  * @throws Error if no API key available for the model
720
748
  */
721
- setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel): Promise<void>;
749
+ setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel, options?: {
750
+ persistAsSessionDefault?: boolean;
751
+ }): Promise<void>;
722
752
  /**
723
753
  * Cycle to next/previous model.
724
754
  * Uses scoped models (from --models flag) if available, otherwise all available models.
@@ -946,6 +976,13 @@ export declare class AgentSession {
946
976
  * Does not persist the record to history. Public so other sessions can forward.
947
977
  */
948
978
  emitIrcRelayObservation(record: CustomMessage): void;
979
+ emitSubagentSteerObservation(args: {
980
+ from: string;
981
+ to: string;
982
+ body: string;
983
+ timestamp?: number;
984
+ }): void;
985
+ emitSubagentSteerRelayObservation(record: CustomMessage): void;
949
986
  /**
950
987
  * Run a single ephemeral side-channel turn against this session's current
951
988
  * model + system prompt + history. No tools are used; the side request
@@ -2,5 +2,5 @@
2
2
  * Re-exports from @gajae-code/ai.
3
3
  * All credential storage types and the AuthStorage class now live in the ai package.
4
4
  */
5
- export type { ApiKeyCredential, AuthCredential, AuthCredentialEntry, AuthCredentialStore, AuthStorageData, AuthStorageOptions, OAuthCredential, SerializedAuthStorage, StoredAuthCredential, } from "@gajae-code/ai";
5
+ export type { ApiKeyCredential, AuthCredential, AuthCredentialEntry, AuthCredentialIfAbsentReason, AuthCredentialIfAbsentResult, AuthCredentialIfAbsentSnapshotResult, AuthCredentialStore, AuthStorageData, AuthStorageOptions, OAuthCredential, SerializedAuthStorage, StoredAuthCredential, } from "@gajae-code/ai";
6
6
  export { AuthBrokerClient, AuthStorage, REMOTE_REFRESH_SENTINEL, RemoteAuthCredentialStore, SqliteAuthCredentialStore, } from "@gajae-code/ai";