@gajae-code/coding-agent 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/notifications/config.d.ts +8 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +15 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +7 -7
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +37 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/notify-cli.ts +91 -7
- package/src/cli.ts +3 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
- package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +13 -0
- package/src/gjc-runtime/session-state-sidecar.ts +59 -11
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/team-runtime.ts +23 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
- package/src/hooks/native-skill-hook.ts +29 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +52 -41
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +179 -17
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +43 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/notifications/config.ts +9 -1
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +39 -9
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +3 -1
- package/src/notifications/telegram-daemon.ts +217 -43
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/computer.md +2 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +20 -6
- package/src/session/agent-session.ts +225 -31
- package/src/session/agent-storage.ts +4 -0
- package/src/session/session-manager.ts +44 -2
- package/src/slash-commands/builtin-registry.ts +153 -71
- package/src/task/executor.ts +7 -4
- package/src/task/index.ts +9 -3
- package/src/task/output-manager.ts +16 -6
- package/src/task/receipt.ts +26 -7
- package/src/tools/computer.ts +246 -43
- package/src/tools/telegram-send.ts +6 -0
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
} from "./index";
|
|
28
28
|
import {
|
|
29
29
|
formatLifecycleOutcome,
|
|
30
|
+
isLifecycleCommandLikeText,
|
|
30
31
|
isLifecycleCommandText,
|
|
31
32
|
lifecycleUsage,
|
|
32
33
|
parseLifecycleCommand,
|
|
@@ -48,7 +49,6 @@ import {
|
|
|
48
49
|
buildActionMessage,
|
|
49
50
|
type CallbackRoute,
|
|
50
51
|
createAliasTable,
|
|
51
|
-
type PendingAsk,
|
|
52
52
|
readEndpoint,
|
|
53
53
|
routeInboundUpdate,
|
|
54
54
|
} from "./telegram-reference";
|
|
@@ -56,7 +56,7 @@ import { decideThreadedInbound, type InboundAttachment } from "./threaded-inboun
|
|
|
56
56
|
import { renderThreadedFrame, type ThreadedSend } from "./threaded-render";
|
|
57
57
|
import { TopicRegistry, type TopicRegistryState } from "./topic-registry";
|
|
58
58
|
|
|
59
|
-
export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled";
|
|
59
|
+
export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled" | "blocked";
|
|
60
60
|
|
|
61
61
|
export interface DaemonState {
|
|
62
62
|
pid: number;
|
|
@@ -126,6 +126,7 @@ const TYPING_REFRESH_INTERVAL_MS = 4_000;
|
|
|
126
126
|
// messages: queued on receipt, consumed once a turn picks the message up.
|
|
127
127
|
const QUEUED_REACTION = "👀";
|
|
128
128
|
const PENDING_TOPIC_FRAME_LIMIT = 20;
|
|
129
|
+
const SEEN_UPDATE_ID_LIMIT = 1_000;
|
|
129
130
|
const CONSUMED_REACTION = "✅";
|
|
130
131
|
|
|
131
132
|
function splitTelegramPlainText(text: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
|
|
@@ -267,7 +268,7 @@ export async function registerNotificationRoot(input: {
|
|
|
267
268
|
const fsImpl = input.fs ?? nodeFs;
|
|
268
269
|
const paths = daemonPaths(input.settings.getAgentDir());
|
|
269
270
|
await ensureDir(fsImpl, paths.dir);
|
|
270
|
-
const root =
|
|
271
|
+
const root = notificationRootForCwd(input.cwd);
|
|
271
272
|
await withFileLock(
|
|
272
273
|
paths.roots,
|
|
273
274
|
async () => {
|
|
@@ -286,6 +287,29 @@ export async function registerNotificationRoot(input: {
|
|
|
286
287
|
return root;
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
function notificationRootForCwd(cwd: string): string {
|
|
291
|
+
return path.join(cwd, ".gjc", "state");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ownerIdentityMatches(state: DaemonState, tokenFingerprint: string, chatId: string): boolean {
|
|
295
|
+
return state.tokenFingerprint === tokenFingerprint && state.chatId === chatId;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function liveOwnerUsesDifferentIdentity(input: {
|
|
299
|
+
state: DaemonState | undefined;
|
|
300
|
+
tokenFingerprint: string;
|
|
301
|
+
chatId: string;
|
|
302
|
+
pidAlive: (pid: number) => boolean;
|
|
303
|
+
}): boolean {
|
|
304
|
+
const { state } = input;
|
|
305
|
+
return Boolean(
|
|
306
|
+
state &&
|
|
307
|
+
state.version === DAEMON_VERSION &&
|
|
308
|
+
!ownerIdentityMatches(state, input.tokenFingerprint, input.chatId) &&
|
|
309
|
+
input.pidAlive(state.pid),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
289
313
|
export function isFreshLiveOwner(input: {
|
|
290
314
|
state: DaemonState | undefined;
|
|
291
315
|
now: number;
|
|
@@ -297,8 +321,7 @@ export function isFreshLiveOwner(input: {
|
|
|
297
321
|
return Boolean(
|
|
298
322
|
state &&
|
|
299
323
|
state.version === DAEMON_VERSION &&
|
|
300
|
-
state.tokenFingerprint
|
|
301
|
-
state.chatId === input.chatId &&
|
|
324
|
+
ownerIdentityMatches(state, input.tokenFingerprint, input.chatId) &&
|
|
302
325
|
input.now - state.heartbeatAt <= HEARTBEAT_TTL_MS &&
|
|
303
326
|
input.pidAlive(state.pid),
|
|
304
327
|
);
|
|
@@ -314,7 +337,13 @@ export async function acquireDaemonOwnership(input: {
|
|
|
314
337
|
pid?: number;
|
|
315
338
|
pidAlive?: (pid: number) => boolean;
|
|
316
339
|
randomId?: () => string;
|
|
317
|
-
}): Promise<{
|
|
340
|
+
}): Promise<{
|
|
341
|
+
acquired: boolean;
|
|
342
|
+
ownerId?: string;
|
|
343
|
+
attached?: boolean;
|
|
344
|
+
blocked?: boolean;
|
|
345
|
+
reason?: "identity_mismatch";
|
|
346
|
+
}> {
|
|
318
347
|
const fsImpl = input.fs ?? nodeFs;
|
|
319
348
|
const now = input.now ?? Date.now;
|
|
320
349
|
const pid = input.pid ?? process.pid;
|
|
@@ -324,6 +353,16 @@ export async function acquireDaemonOwnership(input: {
|
|
|
324
353
|
const ownerId = input.randomId?.() ?? `${pid}-${now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
325
354
|
const roots = input.roots ?? (await readJson<{ roots?: string[] }>(fsImpl, paths.roots))?.roots ?? [];
|
|
326
355
|
const existing = await readJson<DaemonState>(fsImpl, paths.state);
|
|
356
|
+
if (
|
|
357
|
+
liveOwnerUsesDifferentIdentity({
|
|
358
|
+
state: existing,
|
|
359
|
+
tokenFingerprint: input.tokenFingerprint,
|
|
360
|
+
chatId: input.chatId,
|
|
361
|
+
pidAlive,
|
|
362
|
+
})
|
|
363
|
+
) {
|
|
364
|
+
return { acquired: false, blocked: true, reason: "identity_mismatch" };
|
|
365
|
+
}
|
|
327
366
|
if (
|
|
328
367
|
isFreshLiveOwner({
|
|
329
368
|
state: existing,
|
|
@@ -349,6 +388,16 @@ export async function acquireDaemonOwnership(input: {
|
|
|
349
388
|
return { acquired: true, ownerId };
|
|
350
389
|
}
|
|
351
390
|
const afterLock = await readJson<DaemonState>(fsImpl, paths.state);
|
|
391
|
+
if (
|
|
392
|
+
liveOwnerUsesDifferentIdentity({
|
|
393
|
+
state: afterLock,
|
|
394
|
+
tokenFingerprint: input.tokenFingerprint,
|
|
395
|
+
chatId: input.chatId,
|
|
396
|
+
pidAlive,
|
|
397
|
+
})
|
|
398
|
+
) {
|
|
399
|
+
return { acquired: false, blocked: true, reason: "identity_mismatch" };
|
|
400
|
+
}
|
|
352
401
|
if (
|
|
353
402
|
isFreshLiveOwner({
|
|
354
403
|
state: afterLock,
|
|
@@ -375,6 +424,16 @@ export async function acquireDaemonOwnership(input: {
|
|
|
375
424
|
) {
|
|
376
425
|
return { acquired: false, attached: true };
|
|
377
426
|
}
|
|
427
|
+
if (
|
|
428
|
+
liveOwnerUsesDifferentIdentity({
|
|
429
|
+
state: rechecked,
|
|
430
|
+
tokenFingerprint: input.tokenFingerprint,
|
|
431
|
+
chatId: input.chatId,
|
|
432
|
+
pidAlive,
|
|
433
|
+
})
|
|
434
|
+
) {
|
|
435
|
+
return { acquired: false, blocked: true, reason: "identity_mismatch" };
|
|
436
|
+
}
|
|
378
437
|
if (rechecked && pidAlive(rechecked.pid)) {
|
|
379
438
|
return { acquired: false, attached: true };
|
|
380
439
|
}
|
|
@@ -552,7 +611,16 @@ export async function spawnTelegramDaemonOwner(
|
|
|
552
611
|
ownerId: ownership.ownerId ?? "",
|
|
553
612
|
agentDir,
|
|
554
613
|
});
|
|
555
|
-
if (!ownership.acquired)
|
|
614
|
+
if (!ownership.acquired) {
|
|
615
|
+
if (ownership.blocked) {
|
|
616
|
+
return {
|
|
617
|
+
result: "blocked",
|
|
618
|
+
runtime,
|
|
619
|
+
warnings: ["live telegram daemon uses a different bot token or chat; refusing to attach"],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
return { result: "attached", runtime, warnings: [] };
|
|
623
|
+
}
|
|
556
624
|
const spawnImpl = deps.spawn ?? defaultDaemonSpawn;
|
|
557
625
|
const child = spawnImpl(command, args, {
|
|
558
626
|
detached: true,
|
|
@@ -569,12 +637,17 @@ export async function ensureTelegramDaemonRunning(
|
|
|
569
637
|
): Promise<EnsureDaemonResult> {
|
|
570
638
|
const cfg = getNotificationConfig(input.settings);
|
|
571
639
|
if (!isGloballyConfigured(cfg) || !cfg.botToken || !cfg.chatId) return "disabled";
|
|
572
|
-
const root =
|
|
640
|
+
const root = notificationRootForCwd(input.cwd);
|
|
573
641
|
const fp = tokenFingerprint(cfg.botToken);
|
|
574
642
|
const spawned = await spawnTelegramDaemonOwner(
|
|
575
643
|
{ settings: input.settings, roots: [root], tokenFingerprint: fp, chatId: cfg.chatId },
|
|
576
644
|
deps,
|
|
577
645
|
);
|
|
646
|
+
if (spawned.result === "blocked") {
|
|
647
|
+
logger.warn(`notifications: failed to ensure Telegram daemon: ${spawned.warnings.join("; ")}`);
|
|
648
|
+
return spawned.result;
|
|
649
|
+
}
|
|
650
|
+
await registerNotificationRoot({ ...input, fs: deps.fs });
|
|
578
651
|
return spawned.result;
|
|
579
652
|
}
|
|
580
653
|
|
|
@@ -820,6 +893,8 @@ export class TelegramNotificationDaemon {
|
|
|
820
893
|
private readonly flatIdentitySent = new Set<string>();
|
|
821
894
|
/** Cached result of whether the paired chat is a private chat (flat-fallback gate). */
|
|
822
895
|
private pairedChatPrivate: boolean | undefined;
|
|
896
|
+
/** Bot username from getMe, cached once at owner startup for group/forum command targeting. */
|
|
897
|
+
private botUsername: string | undefined;
|
|
823
898
|
/** Sessions whose agent loop is currently busy (drives the typing indicator). */
|
|
824
899
|
private get busy(): Set<string> {
|
|
825
900
|
return this.dispatchState.busy;
|
|
@@ -1078,8 +1153,10 @@ export class TelegramNotificationDaemon {
|
|
|
1078
1153
|
text: string | undefined,
|
|
1079
1154
|
updateId: number | undefined,
|
|
1080
1155
|
threadId: number | undefined,
|
|
1156
|
+
commandCtx: { chatType?: string; botUsername?: string },
|
|
1081
1157
|
): Promise<boolean> {
|
|
1082
|
-
if (!isLifecycleCommandText(text)) return false;
|
|
1158
|
+
if (!isLifecycleCommandText(text, commandCtx)) return false;
|
|
1159
|
+
if (!(await this.pairedChatIsPrivate())) return true;
|
|
1083
1160
|
const reply = async (body: string): Promise<void> => {
|
|
1084
1161
|
for (const text of splitTelegramPlainText(body)) {
|
|
1085
1162
|
await this.botApi
|
|
@@ -1104,15 +1181,15 @@ export class TelegramNotificationDaemon {
|
|
|
1104
1181
|
}
|
|
1105
1182
|
};
|
|
1106
1183
|
|
|
1184
|
+
const parsed = parseLifecycleCommand(text, commandCtx);
|
|
1185
|
+
if (parsed.kind === "none") return false;
|
|
1107
1186
|
if (!this.lifecycleControlActive) {
|
|
1108
1187
|
await reply("Session lifecycle control is not available right now.");
|
|
1109
1188
|
return true;
|
|
1110
1189
|
}
|
|
1111
1190
|
if (updateId !== undefined && this.dispatchState.seenUpdateIds.has(updateId)) return true;
|
|
1112
|
-
if (updateId !== undefined) this.
|
|
1191
|
+
if (updateId !== undefined) await this.rememberSeenUpdateId(updateId);
|
|
1113
1192
|
|
|
1114
|
-
const parsed = parseLifecycleCommand(text);
|
|
1115
|
-
if (parsed.kind === "none") return false;
|
|
1116
1193
|
if (parsed.kind === "usage" || parsed.kind === "reject") {
|
|
1117
1194
|
await reply(parsed.message);
|
|
1118
1195
|
return true;
|
|
@@ -1145,6 +1222,17 @@ export class TelegramNotificationDaemon {
|
|
|
1145
1222
|
return true;
|
|
1146
1223
|
}
|
|
1147
1224
|
|
|
1225
|
+
private async refreshBotIdentity(): Promise<void> {
|
|
1226
|
+
try {
|
|
1227
|
+
const response = (await this.botApi.call("getMe", {})) as { result?: { username?: unknown } };
|
|
1228
|
+
const username = response.result?.username;
|
|
1229
|
+
this.botUsername =
|
|
1230
|
+
typeof username === "string" && username.trim() ? username.trim().replace(/^@/, "") : undefined;
|
|
1231
|
+
} catch {
|
|
1232
|
+
this.botUsername = undefined;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1148
1236
|
/** Map a lifecycle response/error to a user-facing message (G010 surfacing). */
|
|
1149
1237
|
private formatLifecycleResponse(r: SessionLifecycleResponse): string {
|
|
1150
1238
|
return formatLifecycleOutcome(r);
|
|
@@ -1247,6 +1335,51 @@ export class TelegramNotificationDaemon {
|
|
|
1247
1335
|
await writeJsonAtomic(this.fsImpl, paths.aliases, this.aliasTable.serialize());
|
|
1248
1336
|
}
|
|
1249
1337
|
|
|
1338
|
+
async loadSeenUpdateIds(): Promise<void> {
|
|
1339
|
+
const raw = await readJson<{ updateIds?: unknown }>(
|
|
1340
|
+
this.fsImpl,
|
|
1341
|
+
daemonPaths(this.opts.settings.getAgentDir()).seenUpdates,
|
|
1342
|
+
);
|
|
1343
|
+
this.dispatchState.seenUpdateIds.clear();
|
|
1344
|
+
const updateIds = Array.isArray(raw?.updateIds) ? raw.updateIds : [];
|
|
1345
|
+
for (const updateId of updateIds) {
|
|
1346
|
+
if (Number.isSafeInteger(updateId) && Number(updateId) >= 0) {
|
|
1347
|
+
this.dispatchState.seenUpdateIds.add(Number(updateId));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
this.pruneSeenUpdateIds();
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
async persistSeenUpdateIds(): Promise<void> {
|
|
1354
|
+
const paths = daemonPaths(this.opts.settings.getAgentDir());
|
|
1355
|
+
await ensureDir(this.fsImpl, paths.dir);
|
|
1356
|
+
await writeJsonAtomic(this.fsImpl, paths.seenUpdates, {
|
|
1357
|
+
version: 1,
|
|
1358
|
+
updateIds: [...this.dispatchState.seenUpdateIds].slice(-SEEN_UPDATE_ID_LIMIT),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
private pruneSeenUpdateIds(): void {
|
|
1363
|
+
let extra = this.dispatchState.seenUpdateIds.size - SEEN_UPDATE_ID_LIMIT;
|
|
1364
|
+
if (extra <= 0) return;
|
|
1365
|
+
for (const updateId of this.dispatchState.seenUpdateIds) {
|
|
1366
|
+
this.dispatchState.seenUpdateIds.delete(updateId);
|
|
1367
|
+
extra -= 1;
|
|
1368
|
+
if (extra <= 0) break;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
private async rememberSeenUpdateId(updateId: number): Promise<void> {
|
|
1373
|
+
if (!Number.isSafeInteger(updateId) || updateId < 0) return;
|
|
1374
|
+
this.dispatchState.seenUpdateIds.add(updateId);
|
|
1375
|
+
this.pruneSeenUpdateIds();
|
|
1376
|
+
try {
|
|
1377
|
+
await this.persistSeenUpdateIds();
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
logger.warn(`notifications: failed to persist Telegram update id ${updateId}: ${String(err)}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1250
1383
|
async scanRoots(): Promise<void> {
|
|
1251
1384
|
const paths = daemonPaths(this.opts.settings.getAgentDir());
|
|
1252
1385
|
const rootState = await readJson<{ roots?: string[] }>(this.fsImpl, paths.roots);
|
|
@@ -1366,13 +1499,17 @@ export class TelegramNotificationDaemon {
|
|
|
1366
1499
|
* (so a delayed old close cannot delete a replacement), and best-effort closes
|
|
1367
1500
|
* the socket. `scanRoots()` then reconnects the session.
|
|
1368
1501
|
*/
|
|
1369
|
-
private dropSession(session: SessionSocket,
|
|
1502
|
+
private dropSession(session: SessionSocket, reason: string): void {
|
|
1370
1503
|
const clearIntervalImpl = this.opts.clearIntervalImpl ?? clearInterval;
|
|
1371
1504
|
if (session.pingTimer) {
|
|
1372
1505
|
clearIntervalImpl(session.pingTimer);
|
|
1373
1506
|
session.pingTimer = undefined;
|
|
1374
1507
|
}
|
|
1375
|
-
|
|
1508
|
+
const isCurrentSession = this.sessions.get(session.sessionId) === session;
|
|
1509
|
+
if (isCurrentSession || reason === "session_closed") {
|
|
1510
|
+
this.deleteMessageRoutes(session.sessionId);
|
|
1511
|
+
}
|
|
1512
|
+
if (isCurrentSession) {
|
|
1376
1513
|
this.sessions.delete(session.sessionId);
|
|
1377
1514
|
}
|
|
1378
1515
|
if (session.ws.readyState !== WebSocket.CLOSED) {
|
|
@@ -1382,6 +1519,14 @@ export class TelegramNotificationDaemon {
|
|
|
1382
1519
|
}
|
|
1383
1520
|
}
|
|
1384
1521
|
|
|
1522
|
+
private deleteMessageRoutes(sessionId: string, actionId?: string): void {
|
|
1523
|
+
for (const [messageId, route] of this.messageRoutes.entries()) {
|
|
1524
|
+
if (route.sessionId === sessionId && (actionId === undefined || route.actionId === actionId)) {
|
|
1525
|
+
this.messageRoutes.delete(messageId);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1385
1530
|
private static readonly THREADED_FRAMES = new Set([
|
|
1386
1531
|
"identity_header",
|
|
1387
1532
|
"context_update",
|
|
@@ -1434,6 +1579,13 @@ export class TelegramNotificationDaemon {
|
|
|
1434
1579
|
return undefined;
|
|
1435
1580
|
}
|
|
1436
1581
|
|
|
1582
|
+
private sessionCanClaimIdentity(session: SessionSocket, msg: { repo?: unknown; branch?: unknown }): boolean {
|
|
1583
|
+
const current = this.sessions.get(session.sessionId);
|
|
1584
|
+
if (current) return current === session;
|
|
1585
|
+
const ownerId = this.topicOwnerForIdentity(msg);
|
|
1586
|
+
return !ownerId || ownerId === session.sessionId;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1437
1589
|
private async submitThreadedFrame(sessionId: string, send: ThreadedSend, topicId: string): Promise<void> {
|
|
1438
1590
|
this.pool.submit({
|
|
1439
1591
|
sessionId,
|
|
@@ -1444,6 +1596,11 @@ export class TelegramNotificationDaemon {
|
|
|
1444
1596
|
await this.flushPool();
|
|
1445
1597
|
}
|
|
1446
1598
|
|
|
1599
|
+
private async existingTopicForPrivateChat(sessionId: string): Promise<string | undefined> {
|
|
1600
|
+
if (!(await this.pairedChatIsPrivate())) return undefined;
|
|
1601
|
+
return this.topics.get(sessionId)?.topicId;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1447
1604
|
private rememberPendingThreadedFrame(sessionId: string, send: ThreadedSend, msg: Record<string, unknown>): void {
|
|
1448
1605
|
const frames = this.pendingThreadedFrames.get(sessionId) ?? [];
|
|
1449
1606
|
frames.push({ send, msg });
|
|
@@ -1465,6 +1622,7 @@ export class TelegramNotificationDaemon {
|
|
|
1465
1622
|
* one-time nudge) or drop fail-closed for a non-private chat.
|
|
1466
1623
|
*/
|
|
1467
1624
|
private async ensureTopic(sessionId: string, name: string): Promise<string | undefined> {
|
|
1625
|
+
if (!(await this.pairedChatIsPrivate())) return undefined;
|
|
1468
1626
|
const existing = this.topics.get(sessionId);
|
|
1469
1627
|
if (existing) return existing.topicId;
|
|
1470
1628
|
try {
|
|
@@ -1639,6 +1797,7 @@ export class TelegramNotificationDaemon {
|
|
|
1639
1797
|
private async flushPool(): Promise<void> {
|
|
1640
1798
|
for (const item of this.pool.drain()) {
|
|
1641
1799
|
const { send, topicId } = item.payload;
|
|
1800
|
+
if (topicId && !(await this.pairedChatIsPrivate())) continue;
|
|
1642
1801
|
// Threaded topic when available; otherwise deliver flat to the paired chat.
|
|
1643
1802
|
const threadField = topicId ? { message_thread_id: Number(topicId) } : {};
|
|
1644
1803
|
try {
|
|
@@ -1697,9 +1856,10 @@ export class TelegramNotificationDaemon {
|
|
|
1697
1856
|
}
|
|
1698
1857
|
|
|
1699
1858
|
/**
|
|
1700
|
-
* Resolve
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1859
|
+
* Resolve (and cache successful resolution of) whether the paired `chatId` is a
|
|
1860
|
+
* private chat. Topic and flat delivery are only safe in a private DM; any
|
|
1861
|
+
* non-private chat fails closed, while a transient `getChat` failure fails closed
|
|
1862
|
+
* for the current attempt and is retried later.
|
|
1703
1863
|
*/
|
|
1704
1864
|
private async pairedChatIsPrivate(): Promise<boolean> {
|
|
1705
1865
|
if (this.pairedChatPrivate !== undefined) return this.pairedChatPrivate;
|
|
@@ -1708,15 +1868,16 @@ export class TelegramNotificationDaemon {
|
|
|
1708
1868
|
result?: { type?: string };
|
|
1709
1869
|
};
|
|
1710
1870
|
this.pairedChatPrivate = res.result?.type === "private";
|
|
1711
|
-
|
|
1712
|
-
|
|
1871
|
+
return this.pairedChatPrivate;
|
|
1872
|
+
} catch (e) {
|
|
1873
|
+
logger.warn(`notifications: getChat failed while checking Telegram chat privacy: ${String(e)}`);
|
|
1874
|
+
return false;
|
|
1713
1875
|
}
|
|
1714
|
-
return this.pairedChatPrivate;
|
|
1715
1876
|
}
|
|
1716
1877
|
|
|
1717
1878
|
/** Tell the user once (per daemon run) how to enable Threaded Mode. */
|
|
1718
1879
|
private async notifyThreadedFallback(): Promise<void> {
|
|
1719
|
-
if (this.threadedFallbackNoticeSent) return;
|
|
1880
|
+
if (this.threadedFallbackNoticeSent || !(await this.pairedChatIsPrivate())) return;
|
|
1720
1881
|
this.threadedFallbackNoticeSent = true;
|
|
1721
1882
|
try {
|
|
1722
1883
|
await this.botApi.call("sendMessage", {
|
|
@@ -1761,7 +1922,7 @@ export class TelegramNotificationDaemon {
|
|
|
1761
1922
|
/** Send a single `typing` chat action into a busy session's topic (best-effort). */
|
|
1762
1923
|
private async sendTyping(sessionId: string): Promise<void> {
|
|
1763
1924
|
const topicId = this.topics.get(sessionId)?.topicId;
|
|
1764
|
-
if (!topicId) return;
|
|
1925
|
+
if (!topicId || !(await this.pairedChatIsPrivate())) return;
|
|
1765
1926
|
try {
|
|
1766
1927
|
await this.botApi.call("sendChatAction", {
|
|
1767
1928
|
chat_id: this.opts.chatId,
|
|
@@ -1775,6 +1936,7 @@ export class TelegramNotificationDaemon {
|
|
|
1775
1936
|
|
|
1776
1937
|
/** Set a native reaction on an inbound thread message (best-effort). */
|
|
1777
1938
|
private async setReaction(messageId: number, emoji: string): Promise<void> {
|
|
1939
|
+
if (!(await this.pairedChatIsPrivate())) return;
|
|
1778
1940
|
try {
|
|
1779
1941
|
await this.botApi.call("setMessageReaction", {
|
|
1780
1942
|
chat_id: this.opts.chatId,
|
|
@@ -1802,12 +1964,12 @@ export class TelegramNotificationDaemon {
|
|
|
1802
1964
|
if (typeof msg?.type === "string" && TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type)) {
|
|
1803
1965
|
const send = renderThreadedFrame(msg);
|
|
1804
1966
|
if (!send) return;
|
|
1805
|
-
const existingTopic = this.
|
|
1967
|
+
const existingTopic = await this.existingTopicForPrivateChat(session.sessionId);
|
|
1806
1968
|
if (!send.identity && !existingTopic && !this.flatIdentitySent.has(session.sessionId)) {
|
|
1807
1969
|
this.rememberPendingThreadedFrame(session.sessionId, send, msg as Record<string, unknown>);
|
|
1808
1970
|
return;
|
|
1809
1971
|
}
|
|
1810
|
-
if (send.identity) {
|
|
1972
|
+
if (send.identity && !this.sessionCanClaimIdentity(session, msg)) {
|
|
1811
1973
|
const ownerId = this.topicOwnerForIdentity(msg);
|
|
1812
1974
|
const ownerTopic = ownerId ? this.topics.get(ownerId) : undefined;
|
|
1813
1975
|
if (ownerId && ownerId !== session.sessionId && ownerTopic) {
|
|
@@ -1890,6 +2052,7 @@ export class TelegramNotificationDaemon {
|
|
|
1890
2052
|
await this.persistAliases();
|
|
1891
2053
|
} else if (msg.type === "action_resolved" && msg.id) {
|
|
1892
2054
|
session.pending.delete(msg.id);
|
|
2055
|
+
this.deleteMessageRoutes(session.sessionId, msg.id);
|
|
1893
2056
|
for (const [alias, route] of this.aliasTable.entries()) {
|
|
1894
2057
|
if (route.sessionId === session.sessionId && route.actionId === msg.id) this.aliasTable.delete(alias);
|
|
1895
2058
|
}
|
|
@@ -1897,19 +2060,22 @@ export class TelegramNotificationDaemon {
|
|
|
1897
2060
|
}
|
|
1898
2061
|
}
|
|
1899
2062
|
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2063
|
+
private async answerCallbackQueryBestEffort(callbackId: unknown, text?: string): Promise<void> {
|
|
2064
|
+
if (typeof callbackId !== "string") return;
|
|
2065
|
+
try {
|
|
2066
|
+
await this.botApi.call("answerCallbackQuery", {
|
|
2067
|
+
callback_query_id: callbackId,
|
|
2068
|
+
...(text === undefined ? {} : { text }),
|
|
2069
|
+
});
|
|
2070
|
+
} catch {
|
|
2071
|
+
// Telegram callback acknowledgements only dismiss the client-side spinner;
|
|
2072
|
+
// they must never block the already-validated local reply path.
|
|
1905
2073
|
}
|
|
1906
|
-
|
|
1907
|
-
};
|
|
2074
|
+
}
|
|
1908
2075
|
|
|
1909
2076
|
private async sendStaleGuidance(callbackId: unknown): Promise<void> {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
}
|
|
2077
|
+
await this.answerCallbackQueryBestEffort(callbackId, "Button is stale");
|
|
2078
|
+
if (!(await this.pairedChatIsPrivate())) return;
|
|
1913
2079
|
await this.botApi.call("sendMessage", {
|
|
1914
2080
|
chat_id: this.opts.chatId,
|
|
1915
2081
|
text: "This button is stale after notification daemon restart. Please answer locally in the GJC session or wait for a fresh notification.",
|
|
@@ -1924,12 +2090,18 @@ export class TelegramNotificationDaemon {
|
|
|
1924
2090
|
// session input.
|
|
1925
2091
|
{
|
|
1926
2092
|
const m = (update as { update_id?: number; message?: Record<string, unknown> }).message;
|
|
1927
|
-
const
|
|
2093
|
+
const chat = m?.chat as { id?: unknown; type?: unknown } | undefined;
|
|
2094
|
+
const chatId = chat?.id;
|
|
2095
|
+
const chatType = typeof chat?.type === "string" ? chat.type : undefined;
|
|
1928
2096
|
const cmdText = typeof m?.text === "string" ? m.text : undefined;
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
if (
|
|
2097
|
+
const commandCtx = { chatType, botUsername: this.botUsername };
|
|
2098
|
+
if (m !== undefined && String(chatId) === String(this.opts.chatId)) {
|
|
2099
|
+
if (chatType !== undefined && chatType !== "private" && isLifecycleCommandLikeText(cmdText)) return;
|
|
2100
|
+
if (isLifecycleCommandText(cmdText, commandCtx)) {
|
|
2101
|
+
const updateId = (update as { update_id?: number }).update_id;
|
|
2102
|
+
const threadId = typeof m.message_thread_id === "number" ? (m.message_thread_id as number) : undefined;
|
|
2103
|
+
if (await this.handleLifecycleCommand(cmdText, updateId, threadId, commandCtx)) return;
|
|
2104
|
+
}
|
|
1933
2105
|
}
|
|
1934
2106
|
}
|
|
1935
2107
|
// Threaded injection: a free-text message in a known topic (not a button
|
|
@@ -1954,7 +2126,6 @@ export class TelegramNotificationDaemon {
|
|
|
1954
2126
|
});
|
|
1955
2127
|
if (inbound.kind === "duplicate") return;
|
|
1956
2128
|
if (inbound.kind === "inject") {
|
|
1957
|
-
this.dispatchState.seenUpdateIds.add(inbound.updateId);
|
|
1958
2129
|
const session = this.sessions.get(inbound.sessionId);
|
|
1959
2130
|
if (session?.ws.readyState === WebSocket.OPEN) {
|
|
1960
2131
|
const attachmentResult = inbound.attachment
|
|
@@ -1979,6 +2150,7 @@ export class TelegramNotificationDaemon {
|
|
|
1979
2150
|
token: session.token,
|
|
1980
2151
|
}),
|
|
1981
2152
|
);
|
|
2153
|
+
await this.rememberSeenUpdateId(inbound.updateId);
|
|
1982
2154
|
if (inbound.messageId !== undefined) await this.setReaction(inbound.messageId, QUEUED_REACTION);
|
|
1983
2155
|
return;
|
|
1984
2156
|
}
|
|
@@ -1997,6 +2169,7 @@ export class TelegramNotificationDaemon {
|
|
|
1997
2169
|
},
|
|
1998
2170
|
),
|
|
1999
2171
|
);
|
|
2172
|
+
await this.rememberSeenUpdateId(inbound.updateId);
|
|
2000
2173
|
// User turns get a native delivery double-check: queued on receipt,
|
|
2001
2174
|
// flipped to consumed when the session acks the turn that picks it
|
|
2002
2175
|
// up. Config commands are not user turns and get no reaction.
|
|
@@ -2012,7 +2185,6 @@ export class TelegramNotificationDaemon {
|
|
|
2012
2185
|
const decision = routeInboundUpdate(update, {
|
|
2013
2186
|
aliasTable: this.aliasTable,
|
|
2014
2187
|
messageRoutes: this.messageRoutes,
|
|
2015
|
-
pendingBySession: this.pendingBySession,
|
|
2016
2188
|
pairedChatId: this.opts.chatId,
|
|
2017
2189
|
});
|
|
2018
2190
|
if (decision.kind === "reply") {
|
|
@@ -2021,11 +2193,10 @@ export class TelegramNotificationDaemon {
|
|
|
2021
2193
|
await this.sendStaleGuidance(callbackId);
|
|
2022
2194
|
return;
|
|
2023
2195
|
}
|
|
2024
|
-
if (typeof callbackId === "string")
|
|
2025
|
-
await this.botApi.call("answerCallbackQuery", { callback_query_id: callbackId });
|
|
2026
2196
|
session.ws.send(
|
|
2027
2197
|
JSON.stringify({ type: "reply", id: decision.actionId, answer: decision.answer, token: session.token }),
|
|
2028
2198
|
);
|
|
2199
|
+
await this.answerCallbackQueryBestEffort(callbackId);
|
|
2029
2200
|
} else if (decision.kind === "stale") {
|
|
2030
2201
|
await this.sendStaleGuidance(callbackId);
|
|
2031
2202
|
}
|
|
@@ -2068,9 +2239,11 @@ export class TelegramNotificationDaemon {
|
|
|
2068
2239
|
this.startScanTimer();
|
|
2069
2240
|
this.startTypingTimer();
|
|
2070
2241
|
try {
|
|
2242
|
+
await this.refreshBotIdentity();
|
|
2071
2243
|
await this.registerBotCommands();
|
|
2072
2244
|
await this.loadAliases();
|
|
2073
2245
|
await this.loadTopics();
|
|
2246
|
+
await this.loadSeenUpdateIds();
|
|
2074
2247
|
await this.runScan();
|
|
2075
2248
|
// Owner-only: start the session-lifecycle control server now that
|
|
2076
2249
|
// ownership is confirmed (singleton-safe). Best-effort; degrades.
|
|
@@ -2131,6 +2304,7 @@ export class TelegramNotificationDaemon {
|
|
|
2131
2304
|
// (e.g. after reload) reloads aliases/topics seamlessly.
|
|
2132
2305
|
await this.persistAliases().catch(() => undefined);
|
|
2133
2306
|
await this.persistTopics().catch(() => undefined);
|
|
2307
|
+
await this.persistSeenUpdateIds().catch(() => undefined);
|
|
2134
2308
|
await this.opts.control?.clear?.(this.opts.ownerId).catch(() => undefined);
|
|
2135
2309
|
await releaseDaemonOwnership({
|
|
2136
2310
|
settings: this.opts.settings,
|
|
@@ -197,15 +197,9 @@ export type RouteDecision =
|
|
|
197
197
|
| { kind: "stale"; reason: string }
|
|
198
198
|
| { kind: "ignore" };
|
|
199
199
|
|
|
200
|
-
export interface PendingAsk {
|
|
201
|
-
sessionId: string;
|
|
202
|
-
actionId: string;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
200
|
export interface RouteInboundContext {
|
|
206
201
|
aliasTable: Pick<AliasTable, "get">;
|
|
207
202
|
messageRoutes: Map<string | number, CallbackRoute | Omit<CallbackRoute, "answer">>;
|
|
208
|
-
pendingBySession: (sessionId?: string) => PendingAsk[];
|
|
209
203
|
pairedChatId: string;
|
|
210
204
|
}
|
|
211
205
|
|
|
@@ -250,16 +244,6 @@ export function routeInboundUpdate(update: unknown, ctx: RouteInboundContext): R
|
|
|
250
244
|
if (!route) return { kind: "stale", reason: "unknown_reply_message" };
|
|
251
245
|
return { kind: "reply", ...routeWithAnswer(route, text) };
|
|
252
246
|
}
|
|
253
|
-
|
|
254
|
-
if (text) {
|
|
255
|
-
const allPending = ctx.pendingBySession(undefined);
|
|
256
|
-
if (allPending.length === 1) {
|
|
257
|
-
const [pending] = allPending;
|
|
258
|
-
return { kind: "reply", sessionId: pending!.sessionId, actionId: pending!.actionId, answer: text };
|
|
259
|
-
}
|
|
260
|
-
if (allPending.length > 1) return { kind: "stale", reason: "ambiguous_plain_text" };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
247
|
return { kind: "ignore" };
|
|
264
248
|
}
|
|
265
249
|
|
|
@@ -60,9 +60,11 @@ Use for read-only plan critique. It approves only when execution can proceed wit
|
|
|
60
60
|
|
|
61
61
|
<routing>
|
|
62
62
|
- Clear, low-risk implementation request → implement directly with focused verification.
|
|
63
|
+
- Informational questions, bare `?`, and unambiguous explanatory prompts are answer-only/read-only: answer from available context and do not modify files, run commands, or execute workflows unless the user explicitly asks to change, run, implement, or execute something.
|
|
63
64
|
- Do not invoke `deep-interview`, `ralplan`, `ultragoal`, `team`, or role agents for simple clear implementation requests; direct tools and the default launch path are enough.
|
|
64
65
|
- The runtime records a `workflow-intent-diff` CustomEntry for direct-path traceability; it does not participate in LLM context and is not a reason to slow down direct execution.
|
|
65
66
|
- When a task is clear, bounded, and low-risk, the default action is to make the smallest correct change and verify it, not to interview, plan, open a durable ledger, or delegate.
|
|
67
|
+
- Ambiguous implementation asks with missing target, scope, acceptance criteria, or safety boundary require clarification or the appropriate planning workflow before mutation.
|
|
66
68
|
- Small verification needs do not make a task a planning workflow. Escalate only for real ambiguity, non-trivial architecture/sequence risk, durable multi-goal tracking, or useful coordinated workers.
|
|
67
69
|
- Root-cause phase schema is active only for contradiction, regression, or high-risk transition work; otherwise keep ordinary verification and do not add root-cause ceremony.
|
|
68
70
|
- Vague requirements → use `deep-interview` before planning or execution.
|
|
@@ -98,6 +100,7 @@ Use for read-only plan critique. It approves only when execution can proceed wit
|
|
|
98
100
|
- Be concise and information-dense.
|
|
99
101
|
- Do not narrate progress, ceremony, timing, scope inflation, or session limits.
|
|
100
102
|
- If the user's intent is clear, act without asking. Ask only when the next step is destructive or requires a missing choice that materially changes the outcome.
|
|
103
|
+
- Treat an informational question as a request for an answer, not implicit permission to take action; answer read-only unless the user explicitly asks for a concrete change or command execution.
|
|
101
104
|
- When the user proposes something wrong, say what breaks and what to do instead once; then defer to their call.
|
|
102
105
|
- Never use permission-begging or deferral phrasing ("if you want", "if you'd like", "shall I", "I will now", "next I plan to"). For a destructive next step, state the recommended action and stop for approval. For a non-destructive, clearly correct next step, do it directly in the same turn.
|
|
103
106
|
- Do not defer actionable work. Underpromise and overdeliver: report only what is done or in progress, never announce remaining work instead of doing it.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- Disabled means disabled: when the tool is disabled (`computer.alwaysOn=false` with `computer.enabled` unset/false) or the platform is unsupported, every action including `screenshot` fails with `COMPUTER_DISABLED` and captures nothing.
|
|
8
8
|
- Callable only on Apple Silicon macOS (`arm64` darwin); available by default there, with `computer.alwaysOn=false` as the off-switch and `computer.enabled=true` as the manual enable path.
|
|
9
|
-
- Native execution remains supervisor-gated. If the stop/suspend supervisor is unavailable, stale, suspended, permissioned off, display-stale, or cancelled, the action fails closed with a `COMPUTER_*` code.
|
|
9
|
+
- Native execution remains supervisor-gated. If the stop/suspend supervisor is unavailable, stale, suspended, permissioned off, display-stale, or cancelled, the action fails closed with a `COMPUTER_*` code. Coordinate actions carry the latest known screenshot display epoch when one is available so display-topology changes fail with `COMPUTER_DISPLAY_STALE`.
|
|
10
10
|
- Respect the user's stop/suspend request immediately. Do not loop desktop actions after a stop/suspend/error.
|
|
11
11
|
- The user can stop or suspend the session at any time with the configured kill-switch hotkey (default `Control+Option+Command+Escape`). If you see `COMPUTER_CANCELLED` or `COMPUTER_SUPERVISOR_NOT_LIVE`, stop and wait for the user.
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
Coordinates are screenshot pixels, not CSS pixels and not normalized fractions. Use the latest successful `screenshot` dimensions and origin/scale metadata as the coordinate frame. Do not guess coordinates outside the screenshot bounds.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
For stale-display protection to apply, derive pointer coordinates from a successful screenshot in the same tool session; a screenshot-first `batch` is preferred because later coordinate steps are validated against that screenshot and carry its display epoch into native execution. If a coordinate is out of bounds, the batch stops and reports `COMPUTER_COORD_INVALID`. Always capture a fresh screenshot before acting if the display may have changed.
|
|
18
18
|
|
|
19
19
|
## Actions
|
|
20
20
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RuntimeApiKeyTarget {
|
|
2
|
+
setRuntimeApiKey(provider: string, apiKey: string): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface RuntimeApiKeyModel {
|
|
6
|
+
provider: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function applyCliRuntimeApiKeyOverride(
|
|
10
|
+
target: RuntimeApiKeyTarget,
|
|
11
|
+
apiKey: string | undefined,
|
|
12
|
+
model: RuntimeApiKeyModel | undefined,
|
|
13
|
+
): void {
|
|
14
|
+
if (!apiKey || !model) return;
|
|
15
|
+
target.setRuntimeApiKey(model.provider, apiKey);
|
|
16
|
+
}
|