agent-tempo 1.7.0-beta.8 → 1.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.
- package/CLAUDE.md +29 -6
- package/README.md +2 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/maestro.d.ts +46 -1
- package/dist/activities/maestro.js +69 -20
- package/dist/activities/outbox.d.ts +14 -1
- package/dist/activities/outbox.js +71 -10
- package/dist/activities/resolve.d.ts +118 -17
- package/dist/activities/resolve.js +256 -28
- package/dist/activities/schedule-fire.js +5 -2
- package/dist/adapters/base.d.ts +2 -0
- package/dist/adapters/base.js +9 -0
- package/dist/adapters/claude-api/adapter.js +26 -4
- package/dist/adapters/claude-code/adapter.js +15 -9
- package/dist/adapters/claude-code-headless/adapter.js +22 -4
- package/dist/adapters/copilot/adapter.js +58 -8
- package/dist/adapters/opencode/adapter.js +26 -6
- package/dist/adapters/sdk/base.d.ts +60 -0
- package/dist/adapters/sdk/base.js +83 -0
- package/dist/adapters/sdk/doorbell-client.d.ts +106 -0
- package/dist/adapters/sdk/doorbell-client.js +261 -0
- package/dist/adapters/sdk/idle-backoff.d.ts +71 -0
- package/dist/adapters/sdk/idle-backoff.js +136 -0
- package/dist/cli/command-center-command.js +25 -2
- package/dist/cli/commands.d.ts +28 -0
- package/dist/cli/commands.js +111 -20
- package/dist/cli/config-command.js +14 -0
- package/dist/cli/daemon-command.js +41 -5
- package/dist/cli/daemon.d.ts +245 -5
- package/dist/cli/daemon.js +505 -44
- package/dist/cli/help-text.js +3 -1
- package/dist/cli/mcp.d.ts +26 -2
- package/dist/cli/mcp.js +33 -4
- package/dist/cli/sa-preflight.d.ts +27 -1
- package/dist/cli/sa-preflight.js +29 -5
- package/dist/cli/startup.js +8 -2
- package/dist/cli/upgrade-to-2-command.d.ts +10 -0
- package/dist/cli/upgrade-to-2-command.js +159 -0
- package/dist/cli.js +37 -5
- package/dist/client/core.d.ts +6 -0
- package/dist/client/core.js +81 -34
- package/dist/client/subscribe.d.ts +10 -0
- package/dist/client/subscribe.js +2 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +70 -1
- package/dist/daemon.d.ts +78 -7
- package/dist/daemon.js +313 -29
- package/dist/ensemble/saver.d.ts +10 -0
- package/dist/ensemble/saver.js +18 -6
- package/dist/http/aggregate.d.ts +88 -1
- package/dist/http/aggregate.js +157 -13
- package/dist/http/deliverability.d.ts +68 -0
- package/dist/http/deliverability.js +78 -0
- package/dist/http/doorbell-routes.d.ts +32 -0
- package/dist/http/doorbell-routes.js +72 -0
- package/dist/http/doorbell.d.ts +45 -0
- package/dist/http/doorbell.js +162 -0
- package/dist/http/inner-loop-routes.d.ts +9 -0
- package/dist/http/inner-loop-routes.js +4 -0
- package/dist/http/qa.js +8 -1
- package/dist/http/server.d.ts +9 -0
- package/dist/http/server.js +45 -1
- package/dist/http/snapshot.d.ts +28 -5
- package/dist/http/snapshot.js +16 -6
- package/dist/http/writes.js +24 -2
- package/dist/pi/cue-pump.d.ts +107 -7
- package/dist/pi/cue-pump.js +131 -24
- package/dist/pi/extension.js +11 -0
- package/dist/pi/headless.js +49 -13
- package/dist/pi/install.d.ts +15 -0
- package/dist/pi/install.js +35 -0
- package/dist/pi/mission-control/actions.d.ts +52 -3
- package/dist/pi/mission-control/actions.js +112 -3
- package/dist/pi/mission-control/board.d.ts +104 -0
- package/dist/pi/mission-control/board.js +103 -2
- package/dist/pi/mission-control/extension.d.ts +166 -1
- package/dist/pi/mission-control/extension.js +477 -15
- package/dist/pi/mission-control/render.d.ts +25 -0
- package/dist/pi/mission-control/render.js +142 -8
- package/dist/pi/workflow-client.d.ts +29 -0
- package/dist/pi/workflow-client.js +87 -3
- package/dist/reconcile/orphans.js +8 -9
- package/dist/server-tools.js +1 -1
- package/dist/server.js +11 -1
- package/dist/spawn.d.ts +78 -26
- package/dist/spawn.js +69 -37
- package/dist/tools/broadcast.js +15 -1
- package/dist/tools/cue.js +52 -11
- package/dist/tools/ensemble.js +43 -3
- package/dist/tools/recruit.js +11 -29
- package/dist/tools/who-am-i.d.ts +3 -2
- package/dist/tools/who-am-i.js +9 -2
- package/dist/types.d.ts +42 -0
- package/dist/upgrade/phase-engine.d.ts +160 -0
- package/dist/upgrade/phase-engine.js +555 -0
- package/dist/upgrade/snapshot-v1.d.ts +214 -0
- package/dist/upgrade/snapshot-v1.js +165 -0
- package/dist/utils/action-counters.d.ts +75 -0
- package/dist/utils/action-counters.js +279 -0
- package/dist/utils/format-hosts.js +7 -0
- package/dist/utils/hosts.js +5 -0
- package/dist/utils/sdk-probe.d.ts +12 -0
- package/dist/utils/sdk-probe.js +28 -0
- package/dist/utils/search-attributes.d.ts +76 -4
- package/dist/utils/search-attributes.js +89 -4
- package/dist/utils/suspension.d.ts +99 -0
- package/dist/utils/suspension.js +128 -0
- package/dist/utils/visibility-deadline.js +5 -0
- package/dist/worker.d.ts +16 -1
- package/dist/worker.js +25 -4
- package/dist/workflows/maestro.d.ts +10 -0
- package/dist/workflows/maestro.js +126 -21
- package/dist/workflows/scheduler.js +17 -2
- package/dist/workflows/session.js +251 -14
- package/dist/workflows/signals.d.ts +8 -0
- package/dist/workflows/signals.js +3 -2
- package/package.json +8 -4
- package/workflow-bundle.js +580 -40
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InteractiveAttachment = exports.claudeCodeDescriptor = void 0;
|
|
4
4
|
const base_1 = require("../base");
|
|
5
|
+
const idle_backoff_1 = require("../sdk/idle-backoff");
|
|
5
6
|
const terminal_error_1 = require("../terminal-error");
|
|
7
|
+
const action_counters_1 = require("../../utils/action-counters");
|
|
6
8
|
const config_1 = require("../../config");
|
|
7
9
|
const log = (...args) => console.error('[agent-tempo:poller]', ...args);
|
|
8
10
|
/**
|
|
@@ -21,9 +23,11 @@ exports.claudeCodeDescriptor = {
|
|
|
21
23
|
// heartbeat loop at this interval when the V2 lifecycle path is active.
|
|
22
24
|
heartbeatMs: 60_000,
|
|
23
25
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// #749: one family of backoff curves across the codebase — this poller's
|
|
27
|
+
// error backoff shares the SDK idle-backoff constants (values unchanged:
|
|
28
|
+
// 2s base, 1.5× growth, 30s cap; imported at the top of the file). Static
|
|
29
|
+
// defaults only; the AGENT_TEMPO_SDK_POLL_* env overrides apply to SDK
|
|
30
|
+
// adapters, not here.
|
|
27
31
|
/**
|
|
28
32
|
* Poll a session workflow for pending messages and deliver them via `onMessages`.
|
|
29
33
|
*
|
|
@@ -48,7 +52,7 @@ const POLL_MAX_MS = 30000;
|
|
|
48
52
|
function startMessagePoller(handle, onMessages) {
|
|
49
53
|
let stopped = false;
|
|
50
54
|
let timeout = null;
|
|
51
|
-
let currentInterval =
|
|
55
|
+
let currentInterval = idle_backoff_1.SDK_POLL_BASE_MS;
|
|
52
56
|
let consecutiveErrors = 0;
|
|
53
57
|
const cleanupTimer = () => {
|
|
54
58
|
if (timeout) {
|
|
@@ -56,7 +60,9 @@ function startMessagePoller(handle, onMessages) {
|
|
|
56
60
|
timeout = null;
|
|
57
61
|
}
|
|
58
62
|
};
|
|
59
|
-
|
|
63
|
+
// #753 — meter each poll tick's Temporal calls (pendingMessages query +
|
|
64
|
+
// markDelivered signal) under 'sdk-poller'.
|
|
65
|
+
const poll = () => (0, action_counters_1.withActionSource)('sdk-poller', async () => {
|
|
60
66
|
if (stopped)
|
|
61
67
|
return;
|
|
62
68
|
try {
|
|
@@ -69,7 +75,7 @@ function startMessagePoller(handle, onMessages) {
|
|
|
69
75
|
await handle.signal('markDelivered', ids);
|
|
70
76
|
}
|
|
71
77
|
// Reset backoff on successful poll
|
|
72
|
-
currentInterval =
|
|
78
|
+
currentInterval = idle_backoff_1.SDK_POLL_BASE_MS;
|
|
73
79
|
consecutiveErrors = 0;
|
|
74
80
|
}
|
|
75
81
|
catch (err) {
|
|
@@ -88,15 +94,15 @@ function startMessagePoller(handle, onMessages) {
|
|
|
88
94
|
}
|
|
89
95
|
consecutiveErrors++;
|
|
90
96
|
// Apply exponential backoff on errors
|
|
91
|
-
currentInterval = Math.min(currentInterval *
|
|
97
|
+
currentInterval = Math.min(currentInterval * idle_backoff_1.SDK_POLL_BACKOFF_FACTOR, idle_backoff_1.SDK_POLL_MAX_MS);
|
|
92
98
|
log(`Poll error (attempt ${consecutiveErrors}, next in ${Math.round(currentInterval)}ms):`, err);
|
|
93
99
|
}
|
|
94
100
|
if (!stopped) {
|
|
95
101
|
timeout = setTimeout(poll, currentInterval);
|
|
96
102
|
}
|
|
97
|
-
};
|
|
103
|
+
});
|
|
98
104
|
// Start the first poll
|
|
99
|
-
timeout = setTimeout(poll,
|
|
105
|
+
timeout = setTimeout(poll, idle_backoff_1.SDK_POLL_BASE_MS);
|
|
100
106
|
return () => {
|
|
101
107
|
stopped = true;
|
|
102
108
|
cleanupTimer();
|
|
@@ -80,6 +80,7 @@ const client_1 = require("@temporalio/client");
|
|
|
80
80
|
const base_1 = require("../sdk/base");
|
|
81
81
|
const config_1 = require("../../config");
|
|
82
82
|
const connection_1 = require("../../connection");
|
|
83
|
+
const action_counters_1 = require("../../utils/action-counters");
|
|
83
84
|
const signals_1 = require("../../workflows/signals");
|
|
84
85
|
const stream_json_1 = require("./stream-json");
|
|
85
86
|
const error_mapper_1 = require("./error-mapper");
|
|
@@ -130,7 +131,8 @@ const log = (...args) => {
|
|
|
130
131
|
}).join(' ')}\n`;
|
|
131
132
|
fs.writeSync(2, msg);
|
|
132
133
|
};
|
|
133
|
-
/**
|
|
134
|
+
/** Busy-wait cadence (ms) while a turn is in flight. The IDLE cadence is owned
|
|
135
|
+
* by the inherited `pollBackoff` (#749): 2s base → 30s cap, reset on delivery. */
|
|
134
136
|
const POLL_INTERVAL_MS = 2000;
|
|
135
137
|
/** Workflow-register poll bounds — same as claude-api/opencode. */
|
|
136
138
|
const WORKFLOW_REGISTER_ATTEMPTS = 30;
|
|
@@ -266,6 +268,7 @@ class ClaudeCodeHeadlessAttachment extends base_1.SdkAttachment {
|
|
|
266
268
|
const client = new client_1.Client({
|
|
267
269
|
connection,
|
|
268
270
|
namespace: config.temporalNamespace,
|
|
271
|
+
interceptors: (0, action_counters_1.actionCountingInterceptors)(),
|
|
269
272
|
});
|
|
270
273
|
this.configureV2(client, os.hostname());
|
|
271
274
|
// Wait for the session workflow to register in Temporal (the spawn
|
|
@@ -352,6 +355,10 @@ class ClaudeCodeHeadlessAttachment extends base_1.SdkAttachment {
|
|
|
352
355
|
await cleanup();
|
|
353
356
|
process.exit(1);
|
|
354
357
|
}
|
|
358
|
+
// T1.1 PR-2 — cue doorbell: ding ⇒ immediate poll + backoff reset;
|
|
359
|
+
// connected ⇒ 60s idle ceiling. No token / daemon down ⇒ silent no-op
|
|
360
|
+
// (pure #761 polling).
|
|
361
|
+
this.startDoorbell(config.ensemble, playerIdForWorkflow);
|
|
355
362
|
// PID file so callers can find / kill orphaned adapter processes.
|
|
356
363
|
// #690 — write/unlink the EXACT path the spawner computed (ENV.PID_FILE) so the
|
|
357
364
|
// adapter pid can't diverge from the spawner's; helper fallback for a manual launch.
|
|
@@ -377,7 +384,9 @@ class ClaudeCodeHeadlessAttachment extends base_1.SdkAttachment {
|
|
|
377
384
|
process.on('SIGINT', shutdown);
|
|
378
385
|
process.on('SIGTERM', shutdown);
|
|
379
386
|
// Drive the poll loop until cleanup is requested.
|
|
380
|
-
|
|
387
|
+
// #753 — meter its Temporal calls under 'sdk-poller'.
|
|
388
|
+
await (0, action_counters_1.withActionSource)('sdk-poller', () => this.pollLoop(handle));
|
|
389
|
+
this.stopDoorbell();
|
|
381
390
|
try {
|
|
382
391
|
fs.unlinkSync(pidFile);
|
|
383
392
|
}
|
|
@@ -397,6 +406,9 @@ class ClaudeCodeHeadlessAttachment extends base_1.SdkAttachment {
|
|
|
397
406
|
let processing = false;
|
|
398
407
|
let pollCount = 0;
|
|
399
408
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
409
|
+
// #749: fresh poll loop starts at the fast cadence — never inherit a
|
|
410
|
+
// stale 30s interval across a restart.
|
|
411
|
+
this.pollBackoff.reset();
|
|
400
412
|
while (polling && !this.shouldStop()) {
|
|
401
413
|
pollCount++;
|
|
402
414
|
// Periodic workflow-status check — detect external destroy / completion.
|
|
@@ -434,13 +446,19 @@ class ClaudeCodeHeadlessAttachment extends base_1.SdkAttachment {
|
|
|
434
446
|
}
|
|
435
447
|
catch (err) {
|
|
436
448
|
log(`pendingMessages query failed: ${err?.message ?? err}`);
|
|
437
|
-
|
|
449
|
+
// #749: grow on errors too — don't hammer a wedged worker at 2s.
|
|
450
|
+
// T1.1: pollSleep is doorbell-interruptible — a ding ends it early.
|
|
451
|
+
await this.pollSleep(this.pollBackoff.next(false));
|
|
438
452
|
continue;
|
|
439
453
|
}
|
|
440
454
|
if (messages.length === 0) {
|
|
441
|
-
|
|
455
|
+
// #749: idle backoff — 2s → 30s while nothing is pending (60s
|
|
456
|
+
// ceiling while the T1.1 doorbell is connected; a ding wakes it).
|
|
457
|
+
await this.pollSleep(this.pollBackoff.next(false));
|
|
442
458
|
continue;
|
|
443
459
|
}
|
|
460
|
+
// #749: live conversation — snap back to the fast cadence.
|
|
461
|
+
this.pollBackoff.reset();
|
|
444
462
|
processing = true;
|
|
445
463
|
try {
|
|
446
464
|
const ackIds = messages.map((m) => m.id);
|
|
@@ -74,6 +74,7 @@ const path = __importStar(require("path"));
|
|
|
74
74
|
const client_1 = require("@temporalio/client");
|
|
75
75
|
const config_1 = require("../../config");
|
|
76
76
|
const connection_1 = require("../../connection");
|
|
77
|
+
const action_counters_1 = require("../../utils/action-counters");
|
|
77
78
|
const base_1 = require("../sdk/base");
|
|
78
79
|
// #536 — shared SDK-class system-prompt + MAESTRO_ACK (was inline
|
|
79
80
|
// here pre-#536; moved to the shared module so the post-#536
|
|
@@ -128,11 +129,17 @@ const log = (...args) => {
|
|
|
128
129
|
};
|
|
129
130
|
/** Filter process.env to exclude undefined values (safe to spread as Record<string, string>). */
|
|
130
131
|
const cleanEnv = () => Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined));
|
|
132
|
+
/**
|
|
133
|
+
* Busy-wait cadence (ms) while a turn is in flight. The IDLE cadence is owned
|
|
134
|
+
* by the inherited `pollBackoff` (#749, T0.2): 2s base stretching to a 30s
|
|
135
|
+
* cap on empty polls, snapping back to base on any delivered message.
|
|
136
|
+
*/
|
|
131
137
|
const POLL_INTERVAL_MS = 2000;
|
|
132
138
|
const CREATE_SESSION_TIMEOUT_MS = 45_000;
|
|
133
139
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
134
140
|
const MAX_SESSION_RECREATIONS = 2;
|
|
135
|
-
/** Check workflow status every N polls (
|
|
141
|
+
/** Check workflow status every N polls (30s–7.5min depending on the #749
|
|
142
|
+
* idle-backoff cadence; the busier the player, the fresher the check). */
|
|
136
143
|
const WORKFLOW_STATUS_CHECK_INTERVAL = 15;
|
|
137
144
|
/** Proactively recreate the Copilot session after this idle period (ms). Default 60 min. */
|
|
138
145
|
const SESSION_MAX_IDLE_MS = 60 * 60 * 1000;
|
|
@@ -233,6 +240,7 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
233
240
|
const client = new client_1.Client({
|
|
234
241
|
connection,
|
|
235
242
|
namespace: config.temporalNamespace,
|
|
243
|
+
interceptors: (0, action_counters_1.actionCountingInterceptors)(),
|
|
236
244
|
});
|
|
237
245
|
// Hand the client + host to BaseAttachment so startV2Lifecycle (below) can
|
|
238
246
|
// issue claimAttachment + heartbeat against it. No-op on legacy path.
|
|
@@ -457,6 +465,10 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
457
465
|
const expectedAttachmentId = process.env[config_1.ENV.ATTACHMENT_ID] || undefined;
|
|
458
466
|
handle = await this.startV2Lifecycle(expectedWorkflowId, expectedAttachmentId);
|
|
459
467
|
log(`V2 attachment claimed (attachmentId=${this.token?.attachmentId}${expectedAttachmentId ? ', renewed' : ''})`);
|
|
468
|
+
// T1.1 PR-2 — cue doorbell: ding ⇒ immediate poll tick + backoff
|
|
469
|
+
// reset; connected ⇒ 60s idle ceiling. No token / daemon down ⇒
|
|
470
|
+
// silent no-op (pure #761 polling).
|
|
471
|
+
this.startDoorbell(config.ensemble, playerIdForWorkflow);
|
|
460
472
|
}
|
|
461
473
|
catch (err) {
|
|
462
474
|
log(`ERROR: V2 claimAttachment failed: ${err?.message ?? err}`);
|
|
@@ -506,8 +518,6 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
506
518
|
let sessionRecreations = 0;
|
|
507
519
|
let proactiveRecreations = 0;
|
|
508
520
|
let lastActivityTime = Date.now();
|
|
509
|
-
// interval declared here, assigned after poll is defined
|
|
510
|
-
let interval;
|
|
511
521
|
// Shared cleanup — disconnects session, removes PID file, stops client.
|
|
512
522
|
let shuttingDown = false;
|
|
513
523
|
const cleanup = async () => {
|
|
@@ -515,7 +525,10 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
515
525
|
return;
|
|
516
526
|
shuttingDown = true;
|
|
517
527
|
polling = false;
|
|
518
|
-
|
|
528
|
+
// T1.1 — end the doorbell stream and pop any parked inter-tick sleep
|
|
529
|
+
// so the scheduler chain observes `polling = false` promptly.
|
|
530
|
+
this.stopDoorbell();
|
|
531
|
+
this.wakePollSleep();
|
|
519
532
|
// V2 graceful detach — fires `adapterExited` so the workflow collapses
|
|
520
533
|
// draining → detached immediately per §11.1. No-op if V2 was off or if
|
|
521
534
|
// startV2Lifecycle never ran successfully.
|
|
@@ -597,12 +610,19 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
597
610
|
return false;
|
|
598
611
|
}
|
|
599
612
|
};
|
|
613
|
+
/**
|
|
614
|
+
* One poll tick. Returns the delay (ms) before the next tick; the
|
|
615
|
+
* scheduler (`runPollTick`) re-checks `polling` before arming the timer,
|
|
616
|
+
* so shutdown needs no sentinel return (exit paths call `process.exit`).
|
|
617
|
+
* #749: idle ticks grow the inherited `pollBackoff` toward 30s; delivered
|
|
618
|
+
* messages snap back to the 2s base.
|
|
619
|
+
*/
|
|
600
620
|
const poll = async () => {
|
|
601
621
|
if (!polling || processing)
|
|
602
|
-
return;
|
|
622
|
+
return POLL_INTERVAL_MS; // stopped or busy — scheduler gates on `polling`
|
|
603
623
|
pollCount++;
|
|
604
624
|
// Periodic health check
|
|
605
|
-
if (pollCount % 30 === 0) { // every ~
|
|
625
|
+
if (pollCount % 30 === 0) { // every 30 polls (~60s busy, up to ~15min idle)
|
|
606
626
|
const silenceSec = ((Date.now() - lastEventTime) / 1000).toFixed(0);
|
|
607
627
|
log(`[health] poll #${pollCount}, sessionAlive=${sessionAlive}, lastEvent=${lastEventType} ${silenceSec}s ago`);
|
|
608
628
|
}
|
|
@@ -663,8 +683,9 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
663
683
|
}
|
|
664
684
|
try {
|
|
665
685
|
const messages = await handle.query('pendingMessages');
|
|
686
|
+
// #749: idle backoff — nothing pending, stretch the next poll.
|
|
666
687
|
if (messages.length === 0)
|
|
667
|
-
return;
|
|
688
|
+
return this.pollBackoff.next(false);
|
|
668
689
|
processing = true;
|
|
669
690
|
const ids = messages.map((m) => m.id);
|
|
670
691
|
// Format messages into a single prompt, appending ack instruction for Maestro messages
|
|
@@ -704,6 +725,9 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
704
725
|
lastActivityTime = Date.now();
|
|
705
726
|
sessionAlive = true;
|
|
706
727
|
processing = false;
|
|
728
|
+
// #749: live conversation — snap back to the fast cadence so
|
|
729
|
+
// follow-up cues land at 2s responsiveness.
|
|
730
|
+
return this.pollBackoff.next(true);
|
|
707
731
|
}
|
|
708
732
|
catch (err) {
|
|
709
733
|
processing = false;
|
|
@@ -718,10 +742,36 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
718
742
|
await cleanup();
|
|
719
743
|
process.exit(2);
|
|
720
744
|
}
|
|
745
|
+
// Recovered — retry promptly so pending messages drain.
|
|
746
|
+
return this.pollBackoff.next(true);
|
|
721
747
|
}
|
|
748
|
+
// #749: grow on errors too — don't hammer a failing session/worker.
|
|
749
|
+
return this.pollBackoff.next(false);
|
|
722
750
|
}
|
|
723
751
|
};
|
|
724
|
-
|
|
752
|
+
// #749: self-scheduling poll chain (replaces the fixed-2s setInterval —
|
|
753
|
+
// a tick now schedules its successor AFTER it completes, so long ticks
|
|
754
|
+
// can no longer stack skipped-overlap invocations).
|
|
755
|
+
// #753 — meter each poll tick's Temporal calls under 'sdk-poller'.
|
|
756
|
+
const runPollTick = async () => {
|
|
757
|
+
const delay = await (0, action_counters_1.withActionSource)('sdk-poller', poll).catch((err) => {
|
|
758
|
+
// poll() has its own catch — this guards the scheduling chain itself.
|
|
759
|
+
log(`poll tick threw outside its handler: ${err?.message ?? err}`);
|
|
760
|
+
return POLL_INTERVAL_MS;
|
|
761
|
+
});
|
|
762
|
+
if (polling) {
|
|
763
|
+
// T1.1 — the inter-tick wait is doorbell-interruptible: a ding pops
|
|
764
|
+
// it (and reset the backoff) so the next tick runs immediately.
|
|
765
|
+
await this.pollSleep(delay);
|
|
766
|
+
if (polling)
|
|
767
|
+
void runPollTick();
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
this.pollBackoff.reset();
|
|
771
|
+
void this.pollSleep(POLL_INTERVAL_MS).then(() => {
|
|
772
|
+
if (polling)
|
|
773
|
+
void runPollTick();
|
|
774
|
+
});
|
|
725
775
|
log('Message poller started. Bridge is running.');
|
|
726
776
|
// Graceful shutdown on SIGINT/SIGTERM — signal the workflow before exiting
|
|
727
777
|
const shutdown = async () => {
|
|
@@ -71,6 +71,7 @@ const client_1 = require("@temporalio/client");
|
|
|
71
71
|
const base_1 = require("../sdk/base");
|
|
72
72
|
const config_1 = require("../../config");
|
|
73
73
|
const connection_1 = require("../../connection");
|
|
74
|
+
const action_counters_1 = require("../../utils/action-counters");
|
|
74
75
|
const signals_1 = require("../../workflows/signals");
|
|
75
76
|
const server_tools_1 = require("../../server-tools");
|
|
76
77
|
const config_2 = require("./config");
|
|
@@ -130,7 +131,8 @@ const log = (...args) => {
|
|
|
130
131
|
const DEFAULT_MODEL = 'anthropic/claude-opus-4-7';
|
|
131
132
|
/** Tested-pinned OpenCode SDK version — drift triggers a stderr WARNING. */
|
|
132
133
|
const TESTED_OPENCODE_VERSION = '~1.14.29';
|
|
133
|
-
/**
|
|
134
|
+
/** Busy-wait cadence (ms) while a turn is in flight. The IDLE cadence is owned
|
|
135
|
+
* by the inherited `pollBackoff` (#749): 2s base → 30s cap, reset on delivery. */
|
|
134
136
|
const POLL_INTERVAL_MS = 2000;
|
|
135
137
|
/** Workflow-register poll bounds. */
|
|
136
138
|
const WORKFLOW_REGISTER_ATTEMPTS = 30;
|
|
@@ -305,7 +307,11 @@ class OpenCodeAttachment extends base_1.SdkAttachment {
|
|
|
305
307
|
}
|
|
306
308
|
// (5) Connect Temporal, wait for workflow, hand client to BaseAttachment.
|
|
307
309
|
const connection = await (0, connection_1.createTemporalConnection)(config);
|
|
308
|
-
const client = new client_1.Client({
|
|
310
|
+
const client = new client_1.Client({
|
|
311
|
+
connection,
|
|
312
|
+
namespace: config.temporalNamespace,
|
|
313
|
+
interceptors: (0, action_counters_1.actionCountingInterceptors)(),
|
|
314
|
+
});
|
|
309
315
|
this.configureV2(client, os.hostname());
|
|
310
316
|
log(`Waiting for workflow ${expectedWorkflowId} to register...`);
|
|
311
317
|
let handle = client.workflow.getHandle(expectedWorkflowId);
|
|
@@ -379,6 +385,10 @@ class OpenCodeAttachment extends base_1.SdkAttachment {
|
|
|
379
385
|
await cleanup();
|
|
380
386
|
process.exit(1);
|
|
381
387
|
}
|
|
388
|
+
// T1.1 PR-2 — cue doorbell: ding ⇒ immediate poll + backoff reset;
|
|
389
|
+
// connected ⇒ 60s idle ceiling. No token / daemon down ⇒ silent no-op
|
|
390
|
+
// (pure #761 polling).
|
|
391
|
+
this.startDoorbell(config.ensemble, playerIdForWorkflow);
|
|
382
392
|
// (9) PID file — two lines (adapter PID + opencode-serve PID) per
|
|
383
393
|
// design §6.5. Operators can grep / kill either.
|
|
384
394
|
const pidFile = path.join(logDir, `${playerIdForWorkflow}.pid`);
|
|
@@ -401,8 +411,9 @@ class OpenCodeAttachment extends base_1.SdkAttachment {
|
|
|
401
411
|
};
|
|
402
412
|
process.on('SIGINT', shutdown);
|
|
403
413
|
process.on('SIGTERM', shutdown);
|
|
404
|
-
// (11) Drive the poll loop.
|
|
405
|
-
await this.pollLoop(handle);
|
|
414
|
+
// (11) Drive the poll loop. #753 — meter its Temporal calls under 'sdk-poller'.
|
|
415
|
+
await (0, action_counters_1.withActionSource)('sdk-poller', () => this.pollLoop(handle));
|
|
416
|
+
this.stopDoorbell();
|
|
406
417
|
try {
|
|
407
418
|
fs.unlinkSync(pidFile);
|
|
408
419
|
}
|
|
@@ -416,6 +427,9 @@ class OpenCodeAttachment extends base_1.SdkAttachment {
|
|
|
416
427
|
let polling = true;
|
|
417
428
|
let processing = false;
|
|
418
429
|
let pollCount = 0;
|
|
430
|
+
// #749: fresh poll loop starts at the fast cadence — never inherit a
|
|
431
|
+
// stale 30s interval across a restart.
|
|
432
|
+
this.pollBackoff.reset();
|
|
419
433
|
while (polling && !this.shouldStop()) {
|
|
420
434
|
pollCount++;
|
|
421
435
|
// Periodic workflow-status check — detect external destroy / completion.
|
|
@@ -453,13 +467,19 @@ class OpenCodeAttachment extends base_1.SdkAttachment {
|
|
|
453
467
|
}
|
|
454
468
|
catch (err) {
|
|
455
469
|
log(`pendingMessages query failed: ${err?.message ?? err}`);
|
|
456
|
-
|
|
470
|
+
// #749: grow on errors too — don't hammer a wedged worker at 2s.
|
|
471
|
+
// T1.1: pollSleep is doorbell-interruptible — a ding ends it early.
|
|
472
|
+
await this.pollSleep(this.pollBackoff.next(false));
|
|
457
473
|
continue;
|
|
458
474
|
}
|
|
459
475
|
if (messages.length === 0) {
|
|
460
|
-
|
|
476
|
+
// #749: idle backoff — 2s → 30s while nothing is pending (60s
|
|
477
|
+
// ceiling while the T1.1 doorbell is connected; a ding wakes it).
|
|
478
|
+
await this.pollSleep(this.pollBackoff.next(false));
|
|
461
479
|
continue;
|
|
462
480
|
}
|
|
481
|
+
// #749: live conversation — snap back to the fast cadence.
|
|
482
|
+
this.pollBackoff.reset();
|
|
463
483
|
processing = true;
|
|
464
484
|
try {
|
|
465
485
|
const ackIds = messages.map((m) => m.id);
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { WorkflowHandle } from '@temporalio/client';
|
|
28
28
|
import { BaseAttachment, type BaseAttachmentOptions } from '../base';
|
|
29
|
+
import { IdleBackoff } from './idle-backoff';
|
|
29
30
|
import type { Message, DetachReason } from '../../types';
|
|
30
31
|
/** Per-message result from `SdkAttachment.deliver()`. */
|
|
31
32
|
export interface SdkDeliverResult {
|
|
@@ -57,6 +58,31 @@ export declare abstract class SdkAttachment extends BaseAttachment {
|
|
|
57
58
|
* implementations to decide whether cancellation actually has an effect.
|
|
58
59
|
*/
|
|
59
60
|
protected sdkInFlight: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* #749 (T0.2) — shared idle-backoff delay computer for the subclass message
|
|
63
|
+
* poll loops. Contract: `next(messages.length > 0)` per tick — active
|
|
64
|
+
* conversations stay at the 2s base; idle stretches to the 30s cap (15×
|
|
65
|
+
* fewer billable `pendingMessages` queries on an idle player); any
|
|
66
|
+
* delivered message snaps back to base. Subclasses should `reset()` at
|
|
67
|
+
* poll-loop (re)start so a reconnect doesn't inherit a stale slow cadence.
|
|
68
|
+
* Heartbeat/phase-watcher cadences (BaseAttachment) are deliberately NOT
|
|
69
|
+
* driven by this — lease math depends on them (#249).
|
|
70
|
+
*/
|
|
71
|
+
protected readonly pollBackoff: IdleBackoff;
|
|
72
|
+
/**
|
|
73
|
+
* T1.1 PR-2 — cue doorbell (docs/design/t11-cue-doorbell.md §2.4). Started
|
|
74
|
+
* by the concrete adapter via {@link startDoorbell} once it knows its
|
|
75
|
+
* `{ensemble, playerId}`; null until then and after {@link stopDoorbell}.
|
|
76
|
+
* A ding is a HINT: it wakes the poll sleep + resets the backoff — the
|
|
77
|
+
* poll itself (and its acks) remain the sole delivery path.
|
|
78
|
+
*/
|
|
79
|
+
private doorbell;
|
|
80
|
+
/**
|
|
81
|
+
* The ding→immediate-poll primitive (level-triggered, identity-guarded —
|
|
82
|
+
* see {@link WakeableSleep}). Shared shape with the PR-3 Pi pump, which
|
|
83
|
+
* composes the same class without being an SdkAttachment.
|
|
84
|
+
*/
|
|
85
|
+
private readonly pollSleeper;
|
|
60
86
|
constructor(options?: BaseAttachmentOptions);
|
|
61
87
|
/**
|
|
62
88
|
* Deliver one message through the SDK turn. Wire-protocol surface:
|
|
@@ -86,6 +112,40 @@ export declare abstract class SdkAttachment extends BaseAttachment {
|
|
|
86
112
|
* with one `markDelivered` signal — pass the full batch.
|
|
87
113
|
*/
|
|
88
114
|
protected deliver(pinned: WorkflowHandle, msg: Message, prompt: string, timeoutMs: number, invokeSdk: (prompt: string, timeoutMs: number) => Promise<unknown>, ackIds?: string[]): Promise<SdkDeliverResult>;
|
|
115
|
+
/**
|
|
116
|
+
* T1.1 PR-2 — start the doorbell once the concrete adapter knows its
|
|
117
|
+
* identity (typically right after `startV2Lifecycle`). Wires:
|
|
118
|
+
*
|
|
119
|
+
* - `ding` → `pollBackoff.reset()` + wake the parked {@link pollSleep}
|
|
120
|
+
* (or set the pending bit if the loop is mid-tick) ⇒ the very next
|
|
121
|
+
* poll runs now, at the fast cadence.
|
|
122
|
+
* - connection state → idle ceiling: connected ⇒ 60s
|
|
123
|
+
* (`SDK_POLL_DOORBELL_MAX_MS`), disconnected ⇒ the T0.2 30s floor.
|
|
124
|
+
* Degradation is automatic and silent — daemon down means the player
|
|
125
|
+
* behaves exactly like post-#761 (§5 of the design doc).
|
|
126
|
+
*
|
|
127
|
+
* No token / no daemon ⇒ the client never subscribes and this is a no-op
|
|
128
|
+
* beyond one breadcrumb. Idempotent per attachment (restart calls stop
|
|
129
|
+
* first).
|
|
130
|
+
*/
|
|
131
|
+
protected startDoorbell(ensemble: string, playerId: string): void;
|
|
132
|
+
/** Tear down the doorbell stream (graceful detach, adapter exit). Idempotent. */
|
|
133
|
+
protected stopDoorbell(): void;
|
|
134
|
+
/**
|
|
135
|
+
* T1.1 PR-2 — doorbell-interruptible sleep for the subclass poll loops.
|
|
136
|
+
* Drop-in for the previous `await sleep(pollBackoff.next(...))` sites: a
|
|
137
|
+
* `ding` (or {@link wakePollSleep}) resolves it early so the next poll
|
|
138
|
+
* runs immediately; otherwise it behaves exactly like a plain sleep.
|
|
139
|
+
* A ding that landed while the loop was mid-tick resolves the NEXT call
|
|
140
|
+
* instantly (level-triggered pending bit — hints are consumed, not lost).
|
|
141
|
+
*/
|
|
142
|
+
protected pollSleep(ms: number): Promise<void>;
|
|
143
|
+
/**
|
|
144
|
+
* Wake a parked {@link pollSleep} now (ding, or a subclass's own stop
|
|
145
|
+
* path wanting a prompt loop exit). No parked sleep ⇒ sets the pending
|
|
146
|
+
* bit so the next `pollSleep` returns immediately.
|
|
147
|
+
*/
|
|
148
|
+
protected wakePollSleep(): void;
|
|
89
149
|
/**
|
|
90
150
|
* Subclass-facing convenience: call this from `startV2Lifecycle` path to
|
|
91
151
|
* fire `adapterExited` on clean shutdown and tear down the V2 machinery.
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SdkAttachment = void 0;
|
|
4
4
|
const base_1 = require("../base");
|
|
5
5
|
const signals_1 = require("../../workflows/signals");
|
|
6
|
+
const idle_backoff_1 = require("./idle-backoff");
|
|
7
|
+
const doorbell_client_1 = require("./doorbell-client");
|
|
6
8
|
const log = (...args) => console.error('[agent-tempo:sdk-adapter]', ...args);
|
|
7
9
|
/**
|
|
8
10
|
* Abstract base for SDK-class adapters.
|
|
@@ -25,6 +27,31 @@ class SdkAttachment extends base_1.BaseAttachment {
|
|
|
25
27
|
* implementations to decide whether cancellation actually has an effect.
|
|
26
28
|
*/
|
|
27
29
|
sdkInFlight = false;
|
|
30
|
+
/**
|
|
31
|
+
* #749 (T0.2) — shared idle-backoff delay computer for the subclass message
|
|
32
|
+
* poll loops. Contract: `next(messages.length > 0)` per tick — active
|
|
33
|
+
* conversations stay at the 2s base; idle stretches to the 30s cap (15×
|
|
34
|
+
* fewer billable `pendingMessages` queries on an idle player); any
|
|
35
|
+
* delivered message snaps back to base. Subclasses should `reset()` at
|
|
36
|
+
* poll-loop (re)start so a reconnect doesn't inherit a stale slow cadence.
|
|
37
|
+
* Heartbeat/phase-watcher cadences (BaseAttachment) are deliberately NOT
|
|
38
|
+
* driven by this — lease math depends on them (#249).
|
|
39
|
+
*/
|
|
40
|
+
pollBackoff = new idle_backoff_1.IdleBackoff();
|
|
41
|
+
/**
|
|
42
|
+
* T1.1 PR-2 — cue doorbell (docs/design/t11-cue-doorbell.md §2.4). Started
|
|
43
|
+
* by the concrete adapter via {@link startDoorbell} once it knows its
|
|
44
|
+
* `{ensemble, playerId}`; null until then and after {@link stopDoorbell}.
|
|
45
|
+
* A ding is a HINT: it wakes the poll sleep + resets the backoff — the
|
|
46
|
+
* poll itself (and its acks) remain the sole delivery path.
|
|
47
|
+
*/
|
|
48
|
+
doorbell = null;
|
|
49
|
+
/**
|
|
50
|
+
* The ding→immediate-poll primitive (level-triggered, identity-guarded —
|
|
51
|
+
* see {@link WakeableSleep}). Shared shape with the PR-3 Pi pump, which
|
|
52
|
+
* composes the same class without being an SdkAttachment.
|
|
53
|
+
*/
|
|
54
|
+
pollSleeper = new doorbell_client_1.WakeableSleep();
|
|
28
55
|
constructor(options = {}) {
|
|
29
56
|
super(options);
|
|
30
57
|
// Wire the V2 lease-revoked signal (fired by BaseAttachment's phase watcher
|
|
@@ -122,12 +149,68 @@ class SdkAttachment extends base_1.BaseAttachment {
|
|
|
122
149
|
}
|
|
123
150
|
return { sdkResult, elapsedMs };
|
|
124
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* T1.1 PR-2 — start the doorbell once the concrete adapter knows its
|
|
154
|
+
* identity (typically right after `startV2Lifecycle`). Wires:
|
|
155
|
+
*
|
|
156
|
+
* - `ding` → `pollBackoff.reset()` + wake the parked {@link pollSleep}
|
|
157
|
+
* (or set the pending bit if the loop is mid-tick) ⇒ the very next
|
|
158
|
+
* poll runs now, at the fast cadence.
|
|
159
|
+
* - connection state → idle ceiling: connected ⇒ 60s
|
|
160
|
+
* (`SDK_POLL_DOORBELL_MAX_MS`), disconnected ⇒ the T0.2 30s floor.
|
|
161
|
+
* Degradation is automatic and silent — daemon down means the player
|
|
162
|
+
* behaves exactly like post-#761 (§5 of the design doc).
|
|
163
|
+
*
|
|
164
|
+
* No token / no daemon ⇒ the client never subscribes and this is a no-op
|
|
165
|
+
* beyond one breadcrumb. Idempotent per attachment (restart calls stop
|
|
166
|
+
* first).
|
|
167
|
+
*/
|
|
168
|
+
startDoorbell(ensemble, playerId) {
|
|
169
|
+
this.stopDoorbell();
|
|
170
|
+
this.doorbell = new doorbell_client_1.DoorbellClient({
|
|
171
|
+
ensemble,
|
|
172
|
+
playerId,
|
|
173
|
+
onDing: () => {
|
|
174
|
+
this.pollBackoff.reset();
|
|
175
|
+
this.wakePollSleep();
|
|
176
|
+
},
|
|
177
|
+
onConnectionChange: (connected) => {
|
|
178
|
+
this.pollBackoff.setCeiling(connected ? (0, idle_backoff_1.resolveDoorbellCeilingMs)() : (0, idle_backoff_1.resolveIdleBackoffConfig)().maxMs);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
this.doorbell.start();
|
|
182
|
+
}
|
|
183
|
+
/** Tear down the doorbell stream (graceful detach, adapter exit). Idempotent. */
|
|
184
|
+
stopDoorbell() {
|
|
185
|
+
this.doorbell?.stop();
|
|
186
|
+
this.doorbell = null;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* T1.1 PR-2 — doorbell-interruptible sleep for the subclass poll loops.
|
|
190
|
+
* Drop-in for the previous `await sleep(pollBackoff.next(...))` sites: a
|
|
191
|
+
* `ding` (or {@link wakePollSleep}) resolves it early so the next poll
|
|
192
|
+
* runs immediately; otherwise it behaves exactly like a plain sleep.
|
|
193
|
+
* A ding that landed while the loop was mid-tick resolves the NEXT call
|
|
194
|
+
* instantly (level-triggered pending bit — hints are consumed, not lost).
|
|
195
|
+
*/
|
|
196
|
+
pollSleep(ms) {
|
|
197
|
+
return this.pollSleeper.sleep(ms);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Wake a parked {@link pollSleep} now (ding, or a subclass's own stop
|
|
201
|
+
* path wanting a prompt loop exit). No parked sleep ⇒ sets the pending
|
|
202
|
+
* bit so the next `pollSleep` returns immediately.
|
|
203
|
+
*/
|
|
204
|
+
wakePollSleep() {
|
|
205
|
+
this.pollSleeper.wake();
|
|
206
|
+
}
|
|
125
207
|
/**
|
|
126
208
|
* Subclass-facing convenience: call this from `startV2Lifecycle` path to
|
|
127
209
|
* fire `adapterExited` on clean shutdown and tear down the V2 machinery.
|
|
128
210
|
* Forwards to `BaseAttachment.stopV2Lifecycle` with graceful=true.
|
|
129
211
|
*/
|
|
130
212
|
async detachGracefully(reason = 'user-stop') {
|
|
213
|
+
this.stopDoorbell();
|
|
131
214
|
await this.stopV2Lifecycle(reason, /* graceful */ true);
|
|
132
215
|
}
|
|
133
216
|
}
|