@leg3ndy/otto-bridge 0.9.2 → 1.0.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/README.md +78 -17
- package/dist/agentic_runtime/patch/structured_patch.js +240 -0
- package/dist/agentic_runtime/workspace/manager.js +1044 -0
- package/dist/chat_cli_client.js +91 -0
- package/dist/cli_terminal.js +668 -0
- package/dist/executors/native_macos.js +2778 -115
- package/dist/local_automations.js +33 -11
- package/dist/main.js +25 -3
- package/dist/runtime.js +136 -32
- package/dist/runtime_cli_client.js +18 -0
- package/dist/runtime_contract.js +516 -0
- package/dist/tool_catalog.js +148 -1
- package/dist/types.js +2 -2
- package/package.json +7 -2
- package/scripts/postinstall.mjs +35 -0
|
@@ -231,8 +231,12 @@ function isSupportedWhatsAppInboxAutomation(automation) {
|
|
|
231
231
|
const bridgeConfig = automation.bridge_config || {};
|
|
232
232
|
return String(bridgeConfig.monitor_scope || "").trim().toLowerCase() === "inbox";
|
|
233
233
|
}
|
|
234
|
+
function isSupportedBridgeAutomation(automation) {
|
|
235
|
+
return normalizeChannel(automation.channel) === "bridge";
|
|
236
|
+
}
|
|
234
237
|
export class LocalAutomationRuntime {
|
|
235
238
|
config;
|
|
239
|
+
logger;
|
|
236
240
|
automations = new Map();
|
|
237
241
|
states = new Map();
|
|
238
242
|
syncTimer = null;
|
|
@@ -242,8 +246,12 @@ export class LocalAutomationRuntime {
|
|
|
242
246
|
started = false;
|
|
243
247
|
stopped = false;
|
|
244
248
|
whatsappBrowser = null;
|
|
245
|
-
constructor(config) {
|
|
249
|
+
constructor(config, logger) {
|
|
246
250
|
this.config = config;
|
|
251
|
+
this.logger = logger;
|
|
252
|
+
}
|
|
253
|
+
logWarn(message) {
|
|
254
|
+
(this.logger?.warn || console.warn)(message);
|
|
247
255
|
}
|
|
248
256
|
async start() {
|
|
249
257
|
if (this.started) {
|
|
@@ -253,11 +261,11 @@ export class LocalAutomationRuntime {
|
|
|
253
261
|
this.stopped = false;
|
|
254
262
|
await this.syncAutomations().catch((error) => {
|
|
255
263
|
const detail = error instanceof Error ? error.message : String(error);
|
|
256
|
-
|
|
264
|
+
this.logWarn(`[otto-bridge] local automations sync failed: ${detail}`);
|
|
257
265
|
});
|
|
258
266
|
await this.tick().catch((error) => {
|
|
259
267
|
const detail = error instanceof Error ? error.message : String(error);
|
|
260
|
-
|
|
268
|
+
this.logWarn(`[otto-bridge] local automations tick failed: ${detail}`);
|
|
261
269
|
});
|
|
262
270
|
this.syncTimer = setInterval(() => {
|
|
263
271
|
void this.syncAutomations();
|
|
@@ -329,7 +337,7 @@ export class LocalAutomationRuntime {
|
|
|
329
337
|
}
|
|
330
338
|
catch (error) {
|
|
331
339
|
const detail = error instanceof Error ? error.message : String(error);
|
|
332
|
-
|
|
340
|
+
this.logWarn(`[otto-bridge] local automations sync failed: ${detail}`);
|
|
333
341
|
}
|
|
334
342
|
finally {
|
|
335
343
|
this.syncInFlight = false;
|
|
@@ -353,7 +361,10 @@ export class LocalAutomationRuntime {
|
|
|
353
361
|
}
|
|
354
362
|
state.running = true;
|
|
355
363
|
try {
|
|
356
|
-
if (
|
|
364
|
+
if (isSupportedBridgeAutomation(automation)) {
|
|
365
|
+
await this.handleBridgeAutomation(automation);
|
|
366
|
+
}
|
|
367
|
+
else if (isSupportedWhatsAppContactAutomation(automation)) {
|
|
357
368
|
await this.handleWhatsAppContactAutomation(automation, state);
|
|
358
369
|
}
|
|
359
370
|
else if (isSupportedWhatsAppInboxAutomation(automation)) {
|
|
@@ -362,7 +373,7 @@ export class LocalAutomationRuntime {
|
|
|
362
373
|
}
|
|
363
374
|
catch (error) {
|
|
364
375
|
const detail = error instanceof Error ? error.message : String(error);
|
|
365
|
-
|
|
376
|
+
this.logWarn(`[otto-bridge] local automation failed id=${automationId}: ${detail}`);
|
|
366
377
|
}
|
|
367
378
|
finally {
|
|
368
379
|
state.running = false;
|
|
@@ -395,7 +406,7 @@ export class LocalAutomationRuntime {
|
|
|
395
406
|
await browser.ensureReady();
|
|
396
407
|
const selected = await browser.selectConversation(contact);
|
|
397
408
|
if (!selected) {
|
|
398
|
-
|
|
409
|
+
this.logWarn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
|
|
399
410
|
return;
|
|
400
411
|
}
|
|
401
412
|
await this.processWhatsAppConversation(automation, state, browser, contact, { alreadySelected: true });
|
|
@@ -424,7 +435,7 @@ export class LocalAutomationRuntime {
|
|
|
424
435
|
try {
|
|
425
436
|
const selected = await browser.selectConversation(contact);
|
|
426
437
|
if (!selected) {
|
|
427
|
-
|
|
438
|
+
this.logWarn(`[otto-bridge] local whatsapp inbox automation could not find contact="${contact}"`);
|
|
428
439
|
continue;
|
|
429
440
|
}
|
|
430
441
|
await this.processWhatsAppConversation(automation, state, browser, contact, {
|
|
@@ -434,10 +445,21 @@ export class LocalAutomationRuntime {
|
|
|
434
445
|
}
|
|
435
446
|
catch (error) {
|
|
436
447
|
const detail = error instanceof Error ? error.message : String(error);
|
|
437
|
-
|
|
448
|
+
this.logWarn(`[otto-bridge] local whatsapp inbox conversation failed contact="${contact}": ${detail}`);
|
|
438
449
|
}
|
|
439
450
|
}
|
|
440
451
|
}
|
|
452
|
+
async handleBridgeAutomation(automation) {
|
|
453
|
+
const automationId = String(automation.id || "").trim();
|
|
454
|
+
if (!automationId) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
await postDeviceJson(this.config.apiBaseUrl, this.config.deviceToken, "/v1/devices/automations/local/bridge/trigger", {
|
|
458
|
+
automation_id: automationId,
|
|
459
|
+
observed_at: new Date().toISOString(),
|
|
460
|
+
reason: "schedule_tick",
|
|
461
|
+
});
|
|
462
|
+
}
|
|
441
463
|
isPrimaryContactForAutomation(automation, contact) {
|
|
442
464
|
if (!isSupportedWhatsAppContactAutomation(automation)) {
|
|
443
465
|
return false;
|
|
@@ -478,7 +500,7 @@ export class LocalAutomationRuntime {
|
|
|
478
500
|
if (!options?.alreadySelected) {
|
|
479
501
|
const selected = await browser.selectConversation(contact);
|
|
480
502
|
if (!selected) {
|
|
481
|
-
|
|
503
|
+
this.logWarn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
|
|
482
504
|
return;
|
|
483
505
|
}
|
|
484
506
|
}
|
|
@@ -552,7 +574,7 @@ export class LocalAutomationRuntime {
|
|
|
552
574
|
}
|
|
553
575
|
catch (error) {
|
|
554
576
|
const detail = error instanceof Error ? error.message : String(error);
|
|
555
|
-
|
|
577
|
+
this.logWarn(`[otto-bridge] local whatsapp completion failed id=${automation.id}: ${detail}`);
|
|
556
578
|
this.rememberDeltaHash(automation, state, contact, sent ? completionDeltaHash : deltaHash);
|
|
557
579
|
}
|
|
558
580
|
}
|
package/dist/main.js
CHANGED
|
@@ -7,10 +7,15 @@ import { pairDevice } from "./pairing.js";
|
|
|
7
7
|
import { BridgeRuntime } from "./runtime.js";
|
|
8
8
|
import { detectWhatsAppBackgroundStatus, runWhatsAppBackgroundSetup, } from "./whatsapp_background.js";
|
|
9
9
|
import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
|
|
10
|
+
import { launchInteractiveCli, runConsoleCommand, runSetupCommand, } from "./cli_terminal.js";
|
|
10
11
|
const RUNTIME_STATUS_FRESHNESS_MS = 90_000;
|
|
11
12
|
const UPDATE_RETRY_DELAYS_MS = [0, 8_000, 20_000];
|
|
12
13
|
function parseArgs(argv) {
|
|
13
14
|
const [maybeCommand, ...rest] = argv;
|
|
15
|
+
if (!maybeCommand) {
|
|
16
|
+
const interactiveDefault = process.stdout.isTTY && process.stdin.isTTY && process.env.OTTO_BRIDGE_LEGACY_DEFAULT_RUN !== "1";
|
|
17
|
+
return { command: interactiveDefault ? "home" : "run", options: new Map() };
|
|
18
|
+
}
|
|
14
19
|
if (maybeCommand === "--help" || maybeCommand === "-h") {
|
|
15
20
|
return { command: "help", options: new Map() };
|
|
16
21
|
}
|
|
@@ -100,8 +105,11 @@ function resolveExecutorOverrides(args, current) {
|
|
|
100
105
|
}
|
|
101
106
|
function printUsage() {
|
|
102
107
|
console.log(`Usage:
|
|
108
|
+
otto-bridge
|
|
109
|
+
otto-bridge home
|
|
110
|
+
otto-bridge setup
|
|
111
|
+
otto-bridge console
|
|
103
112
|
otto-bridge pair --api http://localhost:8000 --code ABC123 [--name "Meu PC"] [--executor native-macos|mock|clawd-cursor]
|
|
104
|
-
otto-bridge run [--executor native-macos|mock|clawd-cursor] [--clawd-url http://127.0.0.1:3847]
|
|
105
113
|
otto-bridge status
|
|
106
114
|
otto-bridge extensions --list
|
|
107
115
|
otto-bridge extensions --install github
|
|
@@ -113,8 +121,10 @@ function printUsage() {
|
|
|
113
121
|
otto-bridge unpair
|
|
114
122
|
|
|
115
123
|
Examples:
|
|
124
|
+
otto-bridge
|
|
125
|
+
otto-bridge setup
|
|
126
|
+
otto-bridge console
|
|
116
127
|
otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123
|
|
117
|
-
otto-bridge run
|
|
118
128
|
otto-bridge extensions --install whatsappweb
|
|
119
129
|
otto-bridge extensions --setup whatsappweb
|
|
120
130
|
otto-bridge extensions --status whatsappweb
|
|
@@ -275,7 +285,7 @@ async function runPairCommand(args) {
|
|
|
275
285
|
console.log(`[otto-bridge] paired device=${config.deviceId}`);
|
|
276
286
|
console.log(`[otto-bridge] executor=${config.executor.type}`);
|
|
277
287
|
console.log(`[otto-bridge] config=${getBridgeConfigPath()}`);
|
|
278
|
-
console.log("[otto-bridge] next step: run `otto-bridge
|
|
288
|
+
console.log("[otto-bridge] next step: run `otto-bridge` to abrir o hub e manter o runtime local online");
|
|
279
289
|
}
|
|
280
290
|
async function loadRequiredBridgeConfig() {
|
|
281
291
|
const config = await loadBridgeConfig();
|
|
@@ -285,6 +295,7 @@ async function loadRequiredBridgeConfig() {
|
|
|
285
295
|
return config;
|
|
286
296
|
}
|
|
287
297
|
async function runRuntimeCommand(args) {
|
|
298
|
+
console.log("[otto-bridge] `run` agora é um alias legado. Prefira `otto-bridge`.");
|
|
288
299
|
const config = await loadRequiredBridgeConfig();
|
|
289
300
|
const runtimeConfig = {
|
|
290
301
|
...config,
|
|
@@ -574,6 +585,17 @@ async function runUpdateCommand(args) {
|
|
|
574
585
|
async function main() {
|
|
575
586
|
const args = parseArgs(process.argv.slice(2));
|
|
576
587
|
switch (args.command) {
|
|
588
|
+
case "home":
|
|
589
|
+
await launchInteractiveCli();
|
|
590
|
+
return;
|
|
591
|
+
case "setup":
|
|
592
|
+
await runSetupCommand({
|
|
593
|
+
postinstall: args.options.has("postinstall"),
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
case "console":
|
|
597
|
+
await runConsoleCommand(option(args, "prompt"));
|
|
598
|
+
return;
|
|
577
599
|
case "pair":
|
|
578
600
|
await runPairCommand(args);
|
|
579
601
|
return;
|
package/dist/runtime.js
CHANGED
|
@@ -6,6 +6,7 @@ import { JobCancelledError } from "./executors/shared.js";
|
|
|
6
6
|
import { isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
|
|
7
7
|
import { LocalAutomationRuntime } from "./local_automations.js";
|
|
8
8
|
import { buildLocalToolCatalog } from "./tool_catalog.js";
|
|
9
|
+
import { parseJobRuntimeManifest, runtimeStepIdForEvent, } from "./runtime_contract.js";
|
|
9
10
|
function delay(ms) {
|
|
10
11
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
12
|
}
|
|
@@ -55,6 +56,11 @@ function bridgeReleaseFromMessage(message) {
|
|
|
55
56
|
}
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
59
|
+
function confirmationKey(jobId, stepId) {
|
|
60
|
+
const normalizedJobId = String(jobId || "").trim();
|
|
61
|
+
const normalizedStepId = String(stepId || "").trim();
|
|
62
|
+
return normalizedStepId ? `${normalizedJobId}:${normalizedStepId}` : normalizedJobId;
|
|
63
|
+
}
|
|
58
64
|
async function parseSocketMessage(data) {
|
|
59
65
|
if (typeof data === "string") {
|
|
60
66
|
return JSON.parse(data);
|
|
@@ -72,6 +78,7 @@ async function parseSocketMessage(data) {
|
|
|
72
78
|
}
|
|
73
79
|
export class BridgeRuntime {
|
|
74
80
|
config;
|
|
81
|
+
options;
|
|
75
82
|
reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
76
83
|
executor;
|
|
77
84
|
localAutomationRuntime;
|
|
@@ -81,10 +88,23 @@ export class BridgeRuntime {
|
|
|
81
88
|
started = false;
|
|
82
89
|
pendingConfirmations = new Map();
|
|
83
90
|
activeCancels = new Map();
|
|
84
|
-
constructor(config, executor) {
|
|
91
|
+
constructor(config, executor, options = {}) {
|
|
85
92
|
this.config = config;
|
|
93
|
+
this.options = options;
|
|
86
94
|
this.executor = executor ?? this.createDefaultExecutor(config);
|
|
87
|
-
this.localAutomationRuntime = new LocalAutomationRuntime(config);
|
|
95
|
+
this.localAutomationRuntime = new LocalAutomationRuntime(config, this.options.logger);
|
|
96
|
+
}
|
|
97
|
+
logInfo(message) {
|
|
98
|
+
(this.options.logger?.info || console.log)(message);
|
|
99
|
+
}
|
|
100
|
+
logWarn(message) {
|
|
101
|
+
(this.options.logger?.warn || console.warn)(message);
|
|
102
|
+
}
|
|
103
|
+
logError(message) {
|
|
104
|
+
(this.options.logger?.error || console.error)(message);
|
|
105
|
+
}
|
|
106
|
+
emit(event) {
|
|
107
|
+
this.options.logger?.event?.(event);
|
|
88
108
|
}
|
|
89
109
|
async buildHelloMetadata() {
|
|
90
110
|
const metadata = {
|
|
@@ -163,10 +183,11 @@ export class BridgeRuntime {
|
|
|
163
183
|
}
|
|
164
184
|
await this.localAutomationRuntime.start().catch((error) => {
|
|
165
185
|
const detail = error instanceof Error ? error.message : String(error);
|
|
166
|
-
|
|
186
|
+
this.logError(`[otto-bridge] local automation runtime failed to start: ${detail}`);
|
|
167
187
|
});
|
|
168
188
|
}
|
|
169
|
-
|
|
189
|
+
this.logInfo(`[otto-bridge] runtime start device=${this.config.deviceId}`);
|
|
190
|
+
this.emit({ type: "starting", deviceId: this.config.deviceId });
|
|
170
191
|
while (!this.stopped) {
|
|
171
192
|
try {
|
|
172
193
|
await this.connectOnce();
|
|
@@ -177,12 +198,14 @@ export class BridgeRuntime {
|
|
|
177
198
|
break;
|
|
178
199
|
}
|
|
179
200
|
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
-
|
|
201
|
+
this.logError(`[otto-bridge] socket error: ${message}`);
|
|
202
|
+
this.emit({ type: "socket_error", message });
|
|
181
203
|
}
|
|
182
204
|
if (this.stopped) {
|
|
183
205
|
break;
|
|
184
206
|
}
|
|
185
|
-
|
|
207
|
+
this.logInfo(`[otto-bridge] reconnecting in ${this.reconnectDelayMs}ms`);
|
|
208
|
+
this.emit({ type: "reconnecting", delayMs: this.reconnectDelayMs });
|
|
186
209
|
await delay(this.reconnectDelayMs);
|
|
187
210
|
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, DEFAULT_RECONNECT_MAX_DELAY_MS);
|
|
188
211
|
}
|
|
@@ -232,10 +255,11 @@ export class BridgeRuntime {
|
|
|
232
255
|
};
|
|
233
256
|
return await new Promise((resolve, reject) => {
|
|
234
257
|
socket.addEventListener("open", () => {
|
|
235
|
-
|
|
258
|
+
this.logInfo(`[otto-bridge] connected ws=${this.config.wsUrl}`);
|
|
259
|
+
this.emit({ type: "connected", wsUrl: this.config.wsUrl });
|
|
236
260
|
this.sendHello(socket).catch((error) => {
|
|
237
261
|
const detail = error instanceof Error ? error.message : String(error);
|
|
238
|
-
|
|
262
|
+
this.logError(`[otto-bridge] hello metadata failed: ${detail}`);
|
|
239
263
|
});
|
|
240
264
|
heartbeatTimer = setInterval(() => {
|
|
241
265
|
if (socket.readyState === WebSocket.OPEN) {
|
|
@@ -254,14 +278,15 @@ export class BridgeRuntime {
|
|
|
254
278
|
}
|
|
255
279
|
catch (error) {
|
|
256
280
|
const detail = error instanceof Error ? error.message : String(error);
|
|
257
|
-
|
|
281
|
+
this.logError(`[otto-bridge] invalid message: ${detail}`);
|
|
258
282
|
}
|
|
259
283
|
});
|
|
260
284
|
socket.addEventListener("close", (event) => {
|
|
261
285
|
stopHeartbeat();
|
|
262
286
|
rejectPendingConfirmations(new Error("WebSocket closed while awaiting confirmation"));
|
|
263
287
|
this.activeSocket = null;
|
|
264
|
-
|
|
288
|
+
this.logInfo(`[otto-bridge] socket closed code=${event.code}`);
|
|
289
|
+
this.emit({ type: "socket_closed", code: event.code });
|
|
265
290
|
resolve();
|
|
266
291
|
});
|
|
267
292
|
socket.addEventListener("error", () => {
|
|
@@ -282,14 +307,16 @@ export class BridgeRuntime {
|
|
|
282
307
|
const type = String(message.type || "");
|
|
283
308
|
switch (type) {
|
|
284
309
|
case "device.hello":
|
|
285
|
-
|
|
310
|
+
this.logInfo(`[otto-bridge] server hello device=${String(message.device_id || "")}`);
|
|
311
|
+
this.emit({ type: "server_hello", deviceId: String(message.device_id || "") });
|
|
286
312
|
return;
|
|
287
313
|
case "device.hello_ack":
|
|
288
314
|
this.maybeLogBridgeReleaseNotice(message);
|
|
289
315
|
case "device.heartbeat_ack":
|
|
290
316
|
return;
|
|
291
317
|
case "device.job.start":
|
|
292
|
-
|
|
318
|
+
this.logInfo(`[otto-bridge] job start payload=${JSON.stringify(message)}`);
|
|
319
|
+
this.emit({ type: "job_start", jobId: String(message.job_id || "") });
|
|
293
320
|
this.executeJob(socket, {
|
|
294
321
|
job_id: String(message.job_id || ""),
|
|
295
322
|
device_id: String(message.device_id || ""),
|
|
@@ -299,17 +326,17 @@ export class BridgeRuntime {
|
|
|
299
326
|
: {},
|
|
300
327
|
}).catch((error) => {
|
|
301
328
|
const detail = error instanceof Error ? error.message : String(error);
|
|
302
|
-
|
|
329
|
+
this.logError(`[otto-bridge] executor error: ${detail}`);
|
|
303
330
|
});
|
|
304
331
|
return;
|
|
305
332
|
case "device.job.confirmation":
|
|
306
333
|
this.resolveConfirmation(message);
|
|
307
334
|
return;
|
|
308
335
|
case "device.job.cancel":
|
|
309
|
-
await this.cancelJob(String(message.job_id || ""));
|
|
336
|
+
await this.cancelJob(String(message.job_id || ""), String(message.step_id || ""));
|
|
310
337
|
return;
|
|
311
338
|
default:
|
|
312
|
-
|
|
339
|
+
this.logInfo(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
|
|
313
340
|
}
|
|
314
341
|
}
|
|
315
342
|
maybeLogBridgeReleaseNotice(message) {
|
|
@@ -328,49 +355,106 @@ export class BridgeRuntime {
|
|
|
328
355
|
}
|
|
329
356
|
this.lastBridgeReleaseNoticeKey = noticeKey;
|
|
330
357
|
if (updateRequired) {
|
|
331
|
-
|
|
358
|
+
const message = `[otto-bridge] update required current=${this.config.bridgeVersion} min_supported=${minSupportedVersion || "unknown"} latest=${latestVersion || "unknown"} command="${updateCommand}"`;
|
|
359
|
+
this.logWarn(message);
|
|
360
|
+
this.emit({ type: "update_required", message });
|
|
332
361
|
return;
|
|
333
362
|
}
|
|
334
363
|
if (updateAvailable) {
|
|
335
|
-
|
|
364
|
+
const message = `[otto-bridge] update available current=${this.config.bridgeVersion} latest=${latestVersion || "unknown"} command="${updateCommand}"`;
|
|
365
|
+
this.logInfo(message);
|
|
366
|
+
this.emit({ type: "update_available", message });
|
|
336
367
|
}
|
|
337
368
|
}
|
|
369
|
+
clearPendingConfirmations(jobId) {
|
|
370
|
+
const normalizedJobId = String(jobId || "").trim();
|
|
371
|
+
if (!normalizedJobId) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
for (const key of Array.from(this.pendingConfirmations.keys())) {
|
|
375
|
+
if (key === normalizedJobId || key.startsWith(`${normalizedJobId}:`)) {
|
|
376
|
+
this.pendingConfirmations.delete(key);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
resolvePendingCancellation(jobId, stepId) {
|
|
381
|
+
const normalizedJobId = String(jobId || "").trim();
|
|
382
|
+
if (!normalizedJobId) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const keys = stepId
|
|
386
|
+
? [confirmationKey(normalizedJobId, stepId), normalizedJobId]
|
|
387
|
+
: Array.from(this.pendingConfirmations.keys()).filter((key) => (key === normalizedJobId || key.startsWith(`${normalizedJobId}:`)));
|
|
388
|
+
let resolved = false;
|
|
389
|
+
for (const key of keys) {
|
|
390
|
+
const waiter = this.pendingConfirmations.get(key);
|
|
391
|
+
if (!waiter) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
this.pendingConfirmations.delete(key);
|
|
395
|
+
waiter.resolve({
|
|
396
|
+
action: "reject",
|
|
397
|
+
note: "Cancelled by Otto Bridge",
|
|
398
|
+
});
|
|
399
|
+
resolved = true;
|
|
400
|
+
}
|
|
401
|
+
return resolved;
|
|
402
|
+
}
|
|
338
403
|
resolveConfirmation(message) {
|
|
339
404
|
const jobId = String(message.job_id || "");
|
|
405
|
+
const stepId = String(message.step_id || "");
|
|
340
406
|
const action = String(message.action || "").trim().toLowerCase();
|
|
341
|
-
const waiter = this.pendingConfirmations.get(jobId)
|
|
407
|
+
const waiter = this.pendingConfirmations.get(confirmationKey(jobId, stepId))
|
|
408
|
+
|| this.pendingConfirmations.get(jobId);
|
|
342
409
|
if (!jobId || !waiter) {
|
|
343
|
-
|
|
410
|
+
this.logWarn(`[otto-bridge] unexpected confirmation payload=${JSON.stringify(message)}`);
|
|
344
411
|
return;
|
|
345
412
|
}
|
|
346
413
|
if (action !== "approve" && action !== "reject") {
|
|
347
414
|
waiter.reject(new Error(`Unsupported confirmation action: ${action || "unknown"}`));
|
|
415
|
+
this.pendingConfirmations.delete(confirmationKey(jobId, stepId));
|
|
348
416
|
this.pendingConfirmations.delete(jobId);
|
|
349
417
|
return;
|
|
350
418
|
}
|
|
419
|
+
this.pendingConfirmations.delete(confirmationKey(jobId, stepId));
|
|
351
420
|
this.pendingConfirmations.delete(jobId);
|
|
352
421
|
waiter.resolve({
|
|
353
422
|
action,
|
|
354
423
|
note: typeof message.note === "string" ? message.note : undefined,
|
|
355
424
|
});
|
|
356
425
|
}
|
|
357
|
-
async waitForConfirmation(jobId) {
|
|
426
|
+
async waitForConfirmation(jobId, stepId) {
|
|
358
427
|
return await new Promise((resolve, reject) => {
|
|
359
|
-
this.pendingConfirmations.set(jobId, { resolve, reject });
|
|
428
|
+
this.pendingConfirmations.set(confirmationKey(jobId, stepId), { resolve, reject });
|
|
360
429
|
});
|
|
361
430
|
}
|
|
362
|
-
async cancelJob(jobId) {
|
|
431
|
+
async cancelJob(jobId, stepId) {
|
|
363
432
|
if (!jobId) {
|
|
364
433
|
return;
|
|
365
434
|
}
|
|
435
|
+
if (this.resolvePendingCancellation(jobId, stepId)) {
|
|
436
|
+
this.logInfo(`[otto-bridge] confirmation cancelled job=${jobId}${stepId ? ` step=${stepId}` : ""}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
366
439
|
const cancel = this.activeCancels.get(jobId);
|
|
367
440
|
if (!cancel) {
|
|
368
|
-
|
|
441
|
+
this.logWarn(`[otto-bridge] cancel requested for unknown job=${jobId}`);
|
|
369
442
|
return;
|
|
370
443
|
}
|
|
444
|
+
if (stepId) {
|
|
445
|
+
this.logInfo(`[otto-bridge] cancel requested job=${jobId} step=${stepId}`);
|
|
446
|
+
}
|
|
371
447
|
await cancel();
|
|
372
448
|
}
|
|
373
449
|
async executeJob(socket, job) {
|
|
450
|
+
const runtimeManifest = parseJobRuntimeManifest(job);
|
|
451
|
+
const eventStepId = (eventType, options) => {
|
|
452
|
+
const explicitStepId = String(options?.stepId || "").trim();
|
|
453
|
+
if (explicitStepId) {
|
|
454
|
+
return explicitStepId;
|
|
455
|
+
}
|
|
456
|
+
return runtimeStepIdForEvent(runtimeManifest, eventType);
|
|
457
|
+
};
|
|
374
458
|
const sendJson = async (payload) => {
|
|
375
459
|
if (socket.readyState !== WebSocket.OPEN) {
|
|
376
460
|
throw new Error("Socket is not open");
|
|
@@ -378,61 +462,78 @@ export class BridgeRuntime {
|
|
|
378
462
|
socket.send(JSON.stringify(payload));
|
|
379
463
|
};
|
|
380
464
|
this.activeCancels.set(job.job_id, async () => {
|
|
381
|
-
this.
|
|
465
|
+
this.clearPendingConfirmations(job.job_id);
|
|
382
466
|
if (typeof this.executor.cancel === "function") {
|
|
383
467
|
await this.executor.cancel(job.job_id);
|
|
384
468
|
}
|
|
385
|
-
|
|
469
|
+
this.logInfo(`[otto-bridge] job cancelled job_id=${job.job_id}`);
|
|
470
|
+
this.emit({ type: "job_cancelled", jobId: job.job_id });
|
|
386
471
|
});
|
|
387
472
|
try {
|
|
388
473
|
await this.executor.run(job, {
|
|
389
|
-
accepted: async () => {
|
|
474
|
+
accepted: async (options) => {
|
|
475
|
+
const stepId = eventStepId("accepted", options);
|
|
390
476
|
await sendJson({
|
|
391
477
|
type: "device.job.accepted",
|
|
392
478
|
device_id: this.config.deviceId,
|
|
393
479
|
job_id: job.job_id,
|
|
480
|
+
graph_id: runtimeManifest.graphId,
|
|
481
|
+
step_id: stepId,
|
|
394
482
|
accepted_at: Date.now(),
|
|
395
483
|
});
|
|
396
484
|
},
|
|
397
|
-
progress: async (progressPercent, progressMessage) => {
|
|
485
|
+
progress: async (progressPercent, progressMessage, options) => {
|
|
486
|
+
const stepId = eventStepId("progress", options);
|
|
398
487
|
await sendJson({
|
|
399
488
|
type: "device.job.progress",
|
|
400
489
|
device_id: this.config.deviceId,
|
|
401
490
|
job_id: job.job_id,
|
|
491
|
+
graph_id: runtimeManifest.graphId,
|
|
492
|
+
step_id: stepId,
|
|
402
493
|
progress_percent: progressPercent,
|
|
403
494
|
progress_message: progressMessage,
|
|
404
495
|
});
|
|
405
496
|
},
|
|
406
|
-
confirmRequired: async (progressMessage, confirmationContext) => {
|
|
407
|
-
const
|
|
497
|
+
confirmRequired: async (progressMessage, confirmationContext, options) => {
|
|
498
|
+
const stepId = eventStepId("confirm_required", options);
|
|
499
|
+
const confirmationPromise = this.waitForConfirmation(job.job_id, stepId);
|
|
408
500
|
try {
|
|
409
501
|
await sendJson({
|
|
410
502
|
type: "device.job.confirm_required",
|
|
411
503
|
device_id: this.config.deviceId,
|
|
412
504
|
job_id: job.job_id,
|
|
505
|
+
graph_id: runtimeManifest.graphId,
|
|
506
|
+
step_id: stepId,
|
|
413
507
|
progress_message: progressMessage,
|
|
414
508
|
confirmation_context: confirmationContext || {},
|
|
415
509
|
});
|
|
416
510
|
}
|
|
417
511
|
catch (error) {
|
|
512
|
+
this.pendingConfirmations.delete(confirmationKey(job.job_id, stepId));
|
|
418
513
|
this.pendingConfirmations.delete(job.job_id);
|
|
419
514
|
throw error;
|
|
420
515
|
}
|
|
421
516
|
return await confirmationPromise;
|
|
422
517
|
},
|
|
423
|
-
completed: async (result) => {
|
|
518
|
+
completed: async (result, options) => {
|
|
519
|
+
const stepId = eventStepId("completed", options);
|
|
424
520
|
await sendJson({
|
|
425
521
|
type: "device.job.completed",
|
|
426
522
|
device_id: this.config.deviceId,
|
|
427
523
|
job_id: job.job_id,
|
|
524
|
+
graph_id: runtimeManifest.graphId,
|
|
525
|
+
step_id: stepId,
|
|
428
526
|
result: result || {},
|
|
429
527
|
});
|
|
430
528
|
},
|
|
431
|
-
failed: async (errorMessage, result) => {
|
|
529
|
+
failed: async (errorMessage, result, options) => {
|
|
530
|
+
const stepId = eventStepId("failed", options);
|
|
432
531
|
await sendJson({
|
|
433
532
|
type: "device.job.failed",
|
|
434
533
|
device_id: this.config.deviceId,
|
|
435
534
|
job_id: job.job_id,
|
|
535
|
+
graph_id: runtimeManifest.graphId,
|
|
536
|
+
step_id: stepId,
|
|
436
537
|
error_message: errorMessage,
|
|
437
538
|
result: result || {},
|
|
438
539
|
});
|
|
@@ -440,16 +541,19 @@ export class BridgeRuntime {
|
|
|
440
541
|
});
|
|
441
542
|
}
|
|
442
543
|
catch (error) {
|
|
443
|
-
this.
|
|
544
|
+
this.clearPendingConfirmations(job.job_id);
|
|
444
545
|
if (error instanceof JobCancelledError) {
|
|
445
546
|
return;
|
|
446
547
|
}
|
|
447
548
|
const detail = error instanceof Error ? error.message : String(error);
|
|
549
|
+
const stepId = runtimeStepIdForEvent(runtimeManifest, "failed");
|
|
448
550
|
try {
|
|
449
551
|
await sendJson({
|
|
450
552
|
type: "device.job.failed",
|
|
451
553
|
device_id: this.config.deviceId,
|
|
452
554
|
job_id: job.job_id,
|
|
555
|
+
graph_id: runtimeManifest.graphId,
|
|
556
|
+
step_id: stepId,
|
|
453
557
|
error_message: detail || "Executor failed",
|
|
454
558
|
result: {
|
|
455
559
|
executor: this.config.executor.type,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getDeviceJson, postDeviceJson } from "./http.js";
|
|
2
|
+
export async function submitRuntimeCliAssistantPrompt(config, request) {
|
|
3
|
+
return await postDeviceJson(config.apiBaseUrl, config.deviceToken, "/v1/devices/cli/runtime/assistant", request);
|
|
4
|
+
}
|
|
5
|
+
export async function getRuntimeCliJob(config, jobId) {
|
|
6
|
+
return await getDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}`);
|
|
7
|
+
}
|
|
8
|
+
export async function confirmRuntimeCliJob(config, jobId, action, note) {
|
|
9
|
+
return await postDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}/confirm`, {
|
|
10
|
+
action,
|
|
11
|
+
note,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function cancelRuntimeCliJob(config, jobId, note) {
|
|
15
|
+
return await postDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}/cancel`, {
|
|
16
|
+
note,
|
|
17
|
+
});
|
|
18
|
+
}
|