agent-tempo 1.2.0 → 1.3.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/CLAUDE.md +219 -219
- package/LICENSE +21 -21
- package/README.md +289 -289
- package/assets/icon-dark.svg +9 -9
- package/assets/icon.svg +9 -9
- package/assets/logo-dark.svg +11 -11
- package/assets/logo-light.svg +11 -11
- package/dashboard/README.md +91 -91
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +1 -1
- package/dashboard/dist/index.html +19 -19
- package/dashboard/package.json +47 -47
- package/dist/adapters/copilot/adapter.js +12 -1
- package/dist/cli/global-wrapper.d.ts +19 -0
- package/dist/cli/global-wrapper.js +169 -0
- package/dist/cli/help-text.js +97 -97
- package/dist/cli/startup.js +11 -0
- package/dist/cli/upgrade-command.js +81 -81
- package/dist/cli.js +12 -0
- package/dist/daemon.js +5 -0
- package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
- package/dist/server.js +4 -0
- package/dist/spawn.js +12 -12
- package/dist/tools/coat-check-evict.js +2 -2
- package/dist/tools/coat-check-get.js +2 -2
- package/dist/tools/coat-check-put.js +4 -4
- package/dist/tools/fetch-state.js +2 -2
- package/dist/tools/save-state.js +13 -13
- package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
- package/dist/utils/grpc-shutdown-guard.js +88 -0
- package/examples/agents/tempo-composer.md +56 -56
- package/examples/agents/tempo-conductor.md +117 -117
- package/examples/agents/tempo-critic.md +73 -73
- package/examples/agents/tempo-improv.md +74 -74
- package/examples/agents/tempo-liner.md +75 -75
- package/examples/agents/tempo-roadie.md +61 -61
- package/examples/agents/tempo-soloist.md +71 -71
- package/examples/agents/tempo-tuner.md +94 -94
- package/examples/ensembles/tempo-big-band.yaml +146 -146
- package/examples/ensembles/tempo-dev-team.yaml +58 -58
- package/examples/ensembles/tempo-headless-jam.yaml +77 -77
- package/examples/ensembles/tempo-jam-session.yaml +41 -41
- package/examples/ensembles/tempo-mock-jam.yaml +79 -79
- package/examples/ensembles/tempo-review-squad.yaml +32 -32
- package/package.json +173 -173
- package/packaging/launchd/com.agent.tempo.plist +46 -46
- package/packaging/systemd/agent-tempo.service +32 -32
- package/packaging/windows/install-task.ps1 +71 -71
- package/scenarios/conductor-recruit-mock.yaml +33 -33
- package/scenarios/echo-roundtrip.yaml +15 -15
- package/scenarios/multi-player-handoff.yaml +38 -38
- package/scenarios/recruit-cascade.yaml +38 -38
- package/scenarios/two-player-conversation.yaml +33 -33
- package/workflow-bundle.js +1 -1
- package/dist/activities/claude-stop.d.ts +0 -21
- package/dist/activities/claude-stop.js +0 -94
- package/dist/channel.d.ts +0 -3
- package/dist/channel.js +0 -48
- package/dist/copilot-bridge.d.ts +0 -22
- package/dist/copilot-bridge.js +0 -565
- package/dist/scripts/258-spotcheck.js +0 -303
- package/dist/tools/detach.d.ts +0 -4
- package/dist/tools/detach.js +0 -45
- package/dist/tools/encore.d.ts +0 -4
- package/dist/tools/encore.js +0 -31
- package/dist/tools/pause-ensemble.d.ts +0 -4
- package/dist/tools/pause-ensemble.js +0 -58
- package/dist/tools/resume-ensemble.d.ts +0 -4
- package/dist/tools/resume-ensemble.js +0 -79
- package/dist/tools/stop.d.ts +0 -4
- package/dist/tools/stop.js +0 -29
- package/dist/tui/client.d.ts +0 -6
- package/dist/tui/client.js +0 -9
- package/dist/tui/components/ActivityLog.d.ts +0 -16
- package/dist/tui/components/ActivityLog.js +0 -36
- package/dist/tui/components/CommandOverlay.d.ts +0 -15
- package/dist/tui/components/CommandOverlay.js +0 -34
- package/dist/tui/components/ConductorChat.d.ts +0 -16
- package/dist/tui/components/ConductorChat.js +0 -32
- package/dist/tui/components/EnsembleListView.d.ts +0 -14
- package/dist/tui/components/EnsembleListView.js +0 -32
- package/dist/tui/components/EnsemblePanel.d.ts +0 -12
- package/dist/tui/components/EnsemblePanel.js +0 -40
- package/dist/tui/components/InputBar.d.ts +0 -13
- package/dist/tui/components/InputBar.js +0 -58
- package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
- package/dist/tui/components/ScheduleOverlay.js +0 -113
- package/dist/tui/components/TopBar.d.ts +0 -12
- package/dist/tui/components/TopBar.js +0 -15
- package/dist/tui/core-api.d.ts +0 -26
- package/dist/tui/core-api.js +0 -67
- package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
- package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
- package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
- package/dist/tui/hooks/useMaestroPoller.js +0 -36
- package/dist/tui/hooks/useSendCommand.d.ts +0 -7
- package/dist/tui/hooks/useSendCommand.js +0 -29
- package/dist/utils/bg-preflight.d.ts +0 -25
- package/dist/utils/bg-preflight.js +0 -154
|
@@ -131,87 +131,87 @@ async function upgrade(opts) {
|
|
|
131
131
|
// 4. Restarts the daemon
|
|
132
132
|
const cliPid = process.pid;
|
|
133
133
|
const isWin = process.platform === 'win32';
|
|
134
|
-
const updaterScript = `
|
|
135
|
-
const { execFileSync } = require('child_process');
|
|
136
|
-
const fs = require('fs');
|
|
137
|
-
|
|
138
|
-
const PID = ${cliPid};
|
|
139
|
-
const INSTALL_SPEC = ${JSON.stringify(installSpec)};
|
|
140
|
-
const TARGET = ${JSON.stringify(targetVersion)};
|
|
141
|
-
const IS_WIN = ${isWin};
|
|
142
|
-
const LOG_PATH = ${JSON.stringify((0, path_1.join)(config_1.AGENT_TEMPO_HOME, 'upgrade.log'))};
|
|
143
|
-
|
|
144
|
-
function log(msg) {
|
|
145
|
-
const line = new Date().toISOString() + ' ' + msg;
|
|
146
|
-
try { fs.appendFileSync(LOG_PATH, line + '\\n'); } catch {}
|
|
147
|
-
console.log(msg);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function isPidAlive(pid) {
|
|
151
|
-
try { process.kill(pid, 0); return true; }
|
|
152
|
-
catch { return false; }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function main() {
|
|
156
|
-
// Wait for CLI process to exit (up to 10s)
|
|
157
|
-
log('Waiting for CLI process (pid ' + PID + ') to exit...');
|
|
158
|
-
const deadline = Date.now() + 10000;
|
|
159
|
-
while (Date.now() < deadline && isPidAlive(PID)) {
|
|
160
|
-
await new Promise(r => setTimeout(r, 300));
|
|
161
|
-
}
|
|
162
|
-
if (isPidAlive(PID)) {
|
|
163
|
-
log('WARNING: CLI process still alive after 10s, proceeding anyway');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Run npm install -g
|
|
167
|
-
log('Installing ' + INSTALL_SPEC + '...');
|
|
168
|
-
try {
|
|
169
|
-
const npmCmd = IS_WIN ? 'npm.cmd' : 'npm';
|
|
170
|
-
execFileSync(npmCmd, ['install', '-g', INSTALL_SPEC], {
|
|
171
|
-
stdio: 'inherit',
|
|
172
|
-
timeout: 120000,
|
|
173
|
-
});
|
|
174
|
-
log('Install completed');
|
|
175
|
-
} catch (err) {
|
|
176
|
-
log('Install FAILED: ' + err.message);
|
|
177
|
-
log('Recovery: npm install -g ' + INSTALL_SPEC);
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Verify installation
|
|
182
|
-
try {
|
|
183
|
-
const tempoCmd = IS_WIN ? 'agent-tempo.cmd' : 'agent-tempo';
|
|
184
|
-
const ver = execFileSync(tempoCmd, ['--version'], {
|
|
185
|
-
encoding: 'utf8',
|
|
186
|
-
timeout: 10000,
|
|
187
|
-
}).trim();
|
|
188
|
-
log('Verified: ' + ver);
|
|
189
|
-
} catch (err) {
|
|
190
|
-
log('WARNING: Could not verify installation: ' + err.message);
|
|
191
|
-
log('Recovery: npm install -g agent-tempo');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Restart the daemon
|
|
195
|
-
log('Restarting daemon...');
|
|
196
|
-
try {
|
|
197
|
-
const tempoCmd = IS_WIN ? 'agent-tempo.cmd' : 'agent-tempo';
|
|
198
|
-
execFileSync(tempoCmd, ['daemon', 'start'], {
|
|
199
|
-
stdio: 'inherit',
|
|
200
|
-
timeout: 30000,
|
|
201
|
-
});
|
|
202
|
-
log('Daemon restarted');
|
|
203
|
-
} catch (err) {
|
|
204
|
-
log('WARNING: Daemon restart failed: ' + err.message);
|
|
205
|
-
log('Run manually: agent-tempo daemon start');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
log('Upgrade complete!');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
main().catch(err => {
|
|
212
|
-
log('Upgrade failed: ' + err.message);
|
|
213
|
-
process.exit(1);
|
|
214
|
-
});
|
|
134
|
+
const updaterScript = `
|
|
135
|
+
const { execFileSync } = require('child_process');
|
|
136
|
+
const fs = require('fs');
|
|
137
|
+
|
|
138
|
+
const PID = ${cliPid};
|
|
139
|
+
const INSTALL_SPEC = ${JSON.stringify(installSpec)};
|
|
140
|
+
const TARGET = ${JSON.stringify(targetVersion)};
|
|
141
|
+
const IS_WIN = ${isWin};
|
|
142
|
+
const LOG_PATH = ${JSON.stringify((0, path_1.join)(config_1.AGENT_TEMPO_HOME, 'upgrade.log'))};
|
|
143
|
+
|
|
144
|
+
function log(msg) {
|
|
145
|
+
const line = new Date().toISOString() + ' ' + msg;
|
|
146
|
+
try { fs.appendFileSync(LOG_PATH, line + '\\n'); } catch {}
|
|
147
|
+
console.log(msg);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isPidAlive(pid) {
|
|
151
|
+
try { process.kill(pid, 0); return true; }
|
|
152
|
+
catch { return false; }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function main() {
|
|
156
|
+
// Wait for CLI process to exit (up to 10s)
|
|
157
|
+
log('Waiting for CLI process (pid ' + PID + ') to exit...');
|
|
158
|
+
const deadline = Date.now() + 10000;
|
|
159
|
+
while (Date.now() < deadline && isPidAlive(PID)) {
|
|
160
|
+
await new Promise(r => setTimeout(r, 300));
|
|
161
|
+
}
|
|
162
|
+
if (isPidAlive(PID)) {
|
|
163
|
+
log('WARNING: CLI process still alive after 10s, proceeding anyway');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Run npm install -g
|
|
167
|
+
log('Installing ' + INSTALL_SPEC + '...');
|
|
168
|
+
try {
|
|
169
|
+
const npmCmd = IS_WIN ? 'npm.cmd' : 'npm';
|
|
170
|
+
execFileSync(npmCmd, ['install', '-g', INSTALL_SPEC], {
|
|
171
|
+
stdio: 'inherit',
|
|
172
|
+
timeout: 120000,
|
|
173
|
+
});
|
|
174
|
+
log('Install completed');
|
|
175
|
+
} catch (err) {
|
|
176
|
+
log('Install FAILED: ' + err.message);
|
|
177
|
+
log('Recovery: npm install -g ' + INSTALL_SPEC);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Verify installation
|
|
182
|
+
try {
|
|
183
|
+
const tempoCmd = IS_WIN ? 'agent-tempo.cmd' : 'agent-tempo';
|
|
184
|
+
const ver = execFileSync(tempoCmd, ['--version'], {
|
|
185
|
+
encoding: 'utf8',
|
|
186
|
+
timeout: 10000,
|
|
187
|
+
}).trim();
|
|
188
|
+
log('Verified: ' + ver);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
log('WARNING: Could not verify installation: ' + err.message);
|
|
191
|
+
log('Recovery: npm install -g agent-tempo');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Restart the daemon
|
|
195
|
+
log('Restarting daemon...');
|
|
196
|
+
try {
|
|
197
|
+
const tempoCmd = IS_WIN ? 'agent-tempo.cmd' : 'agent-tempo';
|
|
198
|
+
execFileSync(tempoCmd, ['daemon', 'start'], {
|
|
199
|
+
stdio: 'inherit',
|
|
200
|
+
timeout: 30000,
|
|
201
|
+
});
|
|
202
|
+
log('Daemon restarted');
|
|
203
|
+
} catch (err) {
|
|
204
|
+
log('WARNING: Daemon restart failed: ' + err.message);
|
|
205
|
+
log('Run manually: agent-tempo daemon start');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
log('Upgrade complete!');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
main().catch(err => {
|
|
212
|
+
log('Upgrade failed: ' + err.message);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
|
215
215
|
`.trim();
|
|
216
216
|
// Clear previous upgrade log before spawning
|
|
217
217
|
const logPath = (0, path_1.join)(config_1.AGENT_TEMPO_HOME, 'upgrade.log');
|
package/dist/cli.js
CHANGED
|
@@ -61,6 +61,8 @@ const dev_banner_1 = require("./cli/dev-banner");
|
|
|
61
61
|
const types_1 = require("./types");
|
|
62
62
|
const config_1 = require("./config");
|
|
63
63
|
const legacy_migration_1 = require("./cli/legacy-migration");
|
|
64
|
+
const global_wrapper_1 = require("./cli/global-wrapper");
|
|
65
|
+
const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
|
|
64
66
|
/** Package root — cli.js compiles to dist/cli.js, so one level up. Used by the inline `version` handler. */
|
|
65
67
|
const PACKAGE_ROOT = (0, path_1.resolve)(__dirname, '..');
|
|
66
68
|
function parseArgs(argv) {
|
|
@@ -328,6 +330,12 @@ function reportMigrationResult(result) {
|
|
|
328
330
|
process.exitCode = 1;
|
|
329
331
|
}
|
|
330
332
|
async function main() {
|
|
333
|
+
// Neutralize the Temporal/grpc-js "Channel has been shut down" retry-after-
|
|
334
|
+
// close race before any Temporal-touching command runs. See
|
|
335
|
+
// src/utils/grpc-shutdown-guard.ts. Cheap, import-clean, and idempotent — it
|
|
336
|
+
// attaches a single `uncaughtException` listener and pulls in no Temporal/grpc
|
|
337
|
+
// modules, so it is safe above the crash-proof fast paths below.
|
|
338
|
+
(0, grpc_shutdown_guard_1.installGrpcShutdownGuard)();
|
|
331
339
|
const args = parseArgs(process.argv.slice(2));
|
|
332
340
|
const overrides = cliOverrides(args);
|
|
333
341
|
// ADR 0014 §5.4 / gate 4: every dev-mode CLI invocation prints the
|
|
@@ -335,6 +343,10 @@ async function main() {
|
|
|
335
343
|
// self-identify as the dev profile. Banner emits to stderr — keeps
|
|
336
344
|
// `--json` stdout consumers clean.
|
|
337
345
|
(0, dev_banner_1.emitDevBannerIfActive)();
|
|
346
|
+
// Refresh the global wrapper entrypoint pointer so `~/.agent-tempo/bin/agent-tempo`
|
|
347
|
+
// always resolves to the currently-running binary. Cheap (single writeFileSync),
|
|
348
|
+
// idempotent, and best-effort — never blocks or throws.
|
|
349
|
+
(0, global_wrapper_1.refreshEntrypoint)();
|
|
338
350
|
// ── Crash-proof fast paths (#157 PR C) ────────────────────────────────
|
|
339
351
|
// These handlers MUST NOT reach `./cli/commands`, `./cli/preflight`, or
|
|
340
352
|
// any other module that transitively imports `@temporalio/*` / `rxjs` /
|
package/dist/daemon.js
CHANGED
|
@@ -66,6 +66,7 @@ const config_1 = require("./config");
|
|
|
66
66
|
const dev_banner_1 = require("./cli/dev-banner");
|
|
67
67
|
const worker_1 = require("./worker");
|
|
68
68
|
const connection_1 = require("./connection");
|
|
69
|
+
const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
|
|
69
70
|
const daemon_1 = require("./cli/daemon");
|
|
70
71
|
const client_3 = require("./client");
|
|
71
72
|
const orphans_1 = require("./reconcile/orphans");
|
|
@@ -684,6 +685,10 @@ function startCleanupLoop(client, daemonConfig, hostname = os.hostname()) {
|
|
|
684
685
|
};
|
|
685
686
|
}
|
|
686
687
|
async function main() {
|
|
688
|
+
// Neutralize the Temporal/grpc-js "Channel has been shut down" retry-after-
|
|
689
|
+
// close race so a stray retry timer can't kill the long-lived daemon. See
|
|
690
|
+
// src/utils/grpc-shutdown-guard.ts.
|
|
691
|
+
(0, grpc_shutdown_guard_1.installGrpcShutdownGuard)();
|
|
687
692
|
// ADR 0014 §5.4 / gate 4 — dev daemon log self-identifies. Banner fires
|
|
688
693
|
// first so it lands at the top of `~/.agent-tempo-dev/daemon.log` for
|
|
689
694
|
// grep-friendly identification regardless of subsequent log volume.
|
|
@@ -81,30 +81,30 @@ catch (err) {
|
|
|
81
81
|
console.error(err && err.message);
|
|
82
82
|
process.exit(1);
|
|
83
83
|
}
|
|
84
|
-
const detector = `
|
|
85
|
-
// Step 1: load daemon-command (should leave require.cache clean of Temporal).
|
|
86
|
-
require(${JSON.stringify(DAEMON_COMMAND_DIST)});
|
|
87
|
-
// Step 2: simulate a regression — load @temporalio/client directly.
|
|
88
|
-
// This is what would happen if a future edit to daemon-command (or one of
|
|
89
|
-
// its deps) accidentally imported something Temporal-adjacent.
|
|
90
|
-
require(${JSON.stringify(temporalClientPath)});
|
|
91
|
-
// Step 3: run the same detector as test/daemon-command-isolation.test.ts.
|
|
92
|
-
const forbidden = [
|
|
93
|
-
/[\\\\/]@temporalio[\\\\/]/,
|
|
94
|
-
/[\\\\/]rxjs[\\\\/]/,
|
|
95
|
-
/[\\\\/]@grpc[\\\\/]/,
|
|
96
|
-
/[\\\\/]nice-grpc(?:-[^\\\\/]+)?[\\\\/]/,
|
|
97
|
-
/[\\\\/]long[\\\\/]umd[\\\\/]/,
|
|
98
|
-
];
|
|
99
|
-
const hits = Object.keys(require.cache).filter((k) => forbidden.some((re) => re.test(k)));
|
|
100
|
-
if (hits.length > 0) {
|
|
101
|
-
console.log('Detector found ' + hits.length + ' forbidden module(s) in require.cache:');
|
|
102
|
-
for (const h of hits.slice(0, 3)) console.log(' ' + h);
|
|
103
|
-
if (hits.length > 3) console.log(' ... (' + (hits.length - 3) + ' more)');
|
|
104
|
-
process.exit(0);
|
|
105
|
-
}
|
|
106
|
-
console.error('BUG: detector did not flag any of the injected forbidden modules.');
|
|
107
|
-
process.exit(1);
|
|
84
|
+
const detector = `
|
|
85
|
+
// Step 1: load daemon-command (should leave require.cache clean of Temporal).
|
|
86
|
+
require(${JSON.stringify(DAEMON_COMMAND_DIST)});
|
|
87
|
+
// Step 2: simulate a regression — load @temporalio/client directly.
|
|
88
|
+
// This is what would happen if a future edit to daemon-command (or one of
|
|
89
|
+
// its deps) accidentally imported something Temporal-adjacent.
|
|
90
|
+
require(${JSON.stringify(temporalClientPath)});
|
|
91
|
+
// Step 3: run the same detector as test/daemon-command-isolation.test.ts.
|
|
92
|
+
const forbidden = [
|
|
93
|
+
/[\\\\/]@temporalio[\\\\/]/,
|
|
94
|
+
/[\\\\/]rxjs[\\\\/]/,
|
|
95
|
+
/[\\\\/]@grpc[\\\\/]/,
|
|
96
|
+
/[\\\\/]nice-grpc(?:-[^\\\\/]+)?[\\\\/]/,
|
|
97
|
+
/[\\\\/]long[\\\\/]umd[\\\\/]/,
|
|
98
|
+
];
|
|
99
|
+
const hits = Object.keys(require.cache).filter((k) => forbidden.some((re) => re.test(k)));
|
|
100
|
+
if (hits.length > 0) {
|
|
101
|
+
console.log('Detector found ' + hits.length + ' forbidden module(s) in require.cache:');
|
|
102
|
+
for (const h of hits.slice(0, 3)) console.log(' ' + h);
|
|
103
|
+
if (hits.length > 3) console.log(' ... (' + (hits.length - 3) + ' more)');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
console.error('BUG: detector did not flag any of the injected forbidden modules.');
|
|
107
|
+
process.exit(1);
|
|
108
108
|
`;
|
|
109
109
|
const result = (0, child_process_1.spawnSync)(process.execPath, ['-e', detector], {
|
|
110
110
|
stdio: ['ignore', 'inherit', 'inherit'],
|
package/dist/server.js
CHANGED
|
@@ -56,9 +56,13 @@ const server_tools_1 = require("./server-tools");
|
|
|
56
56
|
const adapters_1 = require("./adapters");
|
|
57
57
|
const agent_types_1 = require("./ensemble/agent-types");
|
|
58
58
|
const parent_death_watchdog_1 = require("./utils/parent-death-watchdog");
|
|
59
|
+
const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
|
|
59
60
|
const log = (...args) => console.error('[agent-tempo]', ...args);
|
|
60
61
|
async function main() {
|
|
61
62
|
(0, parent_death_watchdog_1.installParentDeathWatchdog)();
|
|
63
|
+
// Neutralize the Temporal/grpc-js "Channel has been shut down" retry-after-
|
|
64
|
+
// close race. See src/utils/grpc-shutdown-guard.ts.
|
|
65
|
+
(0, grpc_shutdown_guard_1.installGrpcShutdownGuard)();
|
|
62
66
|
// Only activate when explicitly opted in via AGENT_TEMPO_ENSEMBLE
|
|
63
67
|
if (!process.env[config_1.ENV.ENSEMBLE]) {
|
|
64
68
|
log(`${config_1.ENV.ENSEMBLE} not set — MCP server idle (no workflow started)`);
|
package/dist/spawn.js
CHANGED
|
@@ -262,12 +262,12 @@ function spawnInTerminal(claudeArgs, workDir, envVars, options) {
|
|
|
262
262
|
// Append `; exit` so the wrapping shell exits when claude does (clean or killed).
|
|
263
263
|
// Without it, claude exit returns control to the shell prompt and the tab lingers —
|
|
264
264
|
// parity with the Windows WT `closeOnExit: 'always'` + parent-walk fix from #166.
|
|
265
|
-
const osaScript = `
|
|
266
|
-
tell application "Ghostty"
|
|
267
|
-
set cfg to new surface configuration
|
|
268
|
-
set initial working directory of cfg to ${JSON.stringify(workDir)}
|
|
269
|
-
set initial input of cfg to ${JSON.stringify(claudeInvocation + '; exit\n')}
|
|
270
|
-
set win to new window with configuration cfg
|
|
265
|
+
const osaScript = `
|
|
266
|
+
tell application "Ghostty"
|
|
267
|
+
set cfg to new surface configuration
|
|
268
|
+
set initial working directory of cfg to ${JSON.stringify(workDir)}
|
|
269
|
+
set initial input of cfg to ${JSON.stringify(claudeInvocation + '; exit\n')}
|
|
270
|
+
set win to new window with configuration cfg
|
|
271
271
|
end tell`;
|
|
272
272
|
log('Using Ghostty initial-input path');
|
|
273
273
|
const child = (0, child_process_1.spawn)('osascript', ['-e', osaScript], {
|
|
@@ -285,12 +285,12 @@ function spawnInTerminal(claudeArgs, workDir, envVars, options) {
|
|
|
285
285
|
// so any `"` or `\` in paths/args doesn't break the AppleScript parser. Parity with
|
|
286
286
|
// the Ghostty path above.
|
|
287
287
|
const shellCmd = `cd ${shellQuote(workDir)} && ${claudeInvocation} ; exit`;
|
|
288
|
-
const osaScript = `
|
|
289
|
-
tell application "iTerm2"
|
|
290
|
-
set newWindow to (create window with default profile)
|
|
291
|
-
tell current session of newWindow
|
|
292
|
-
write text ${JSON.stringify(shellCmd)}
|
|
293
|
-
end tell
|
|
288
|
+
const osaScript = `
|
|
289
|
+
tell application "iTerm2"
|
|
290
|
+
set newWindow to (create window with default profile)
|
|
291
|
+
tell current session of newWindow
|
|
292
|
+
write text ${JSON.stringify(shellCmd)}
|
|
293
|
+
end tell
|
|
294
294
|
end tell`;
|
|
295
295
|
log('Using iTerm2 write-text path');
|
|
296
296
|
const child = (0, child_process_1.spawn)('osascript', ['-e', osaScript], {
|
|
@@ -17,8 +17,8 @@ const maestro_signals_1 = require("../workflows/maestro-signals");
|
|
|
17
17
|
const helpers_1 = require("./helpers");
|
|
18
18
|
const validation_1 = require("../utils/validation");
|
|
19
19
|
function registerCoatCheckEvictTool(server, client, config, getPlayerId) {
|
|
20
|
-
(0, helpers_1.defineTool)(server, 'coat_check_evict', `Evict a coat-check entry (#318) before its TTL expires. Owner-or-conductor only — non-owners (and non-conductors) get a permission error.
|
|
21
|
-
|
|
20
|
+
(0, helpers_1.defineTool)(server, 'coat_check_evict', `Evict a coat-check entry (#318) before its TTL expires. Owner-or-conductor only — non-owners (and non-conductors) get a permission error.
|
|
21
|
+
|
|
22
22
|
Use to free a slot when this ensemble is at the 20-entry cap and you want to make room. \`evicted: false\` means the ticket was already gone (TTL-expired or evicted by someone else).`, {
|
|
23
23
|
ticket: zod_1.z.string().regex(validation_1.COAT_CHECK_TICKET_REGEX).max(validation_1.COAT_CHECK_TICKET_MAX).describe(`The ticket id returned by an earlier \`coat_check_put\` (≤${validation_1.COAT_CHECK_TICKET_MAX} chars).`),
|
|
24
24
|
}, async (args) => {
|
|
@@ -21,8 +21,8 @@ const maestro_signals_1 = require("../workflows/maestro-signals");
|
|
|
21
21
|
const helpers_1 = require("./helpers");
|
|
22
22
|
const validation_1 = require("../utils/validation");
|
|
23
23
|
function registerCoatCheckGetTool(server, client, config, getPlayerId) {
|
|
24
|
-
(0, helpers_1.defineTool)(server, 'coat_check_get', `Redeem a coat-check ticket (#318) and pull the stashed content. Returns the entry's summary, content body, and audit info — or "not found" when the ticket is missing / expired / evicted (no error, just empty).
|
|
25
|
-
|
|
24
|
+
(0, helpers_1.defineTool)(server, 'coat_check_get', `Redeem a coat-check ticket (#318) and pull the stashed content. Returns the entry's summary, content body, and audit info — or "not found" when the ticket is missing / expired / evicted (no error, just empty).
|
|
25
|
+
|
|
26
26
|
Successful redemptions bump the entry's fetch-audit counters (\`lastFetchedAt\` / \`lastFetchedBy\` / \`fetchCount\`) so the putter can later see whether anyone has redeemed. \`coat_check_list\` won't bump these — only an actual redemption counts.`, {
|
|
27
27
|
ticket: zod_1.z.string().regex(validation_1.COAT_CHECK_TICKET_REGEX).max(validation_1.COAT_CHECK_TICKET_MAX).describe(`The ticket id returned by an earlier \`coat_check_put\` (≤${validation_1.COAT_CHECK_TICKET_MAX} chars).`),
|
|
28
28
|
}, async (args) => {
|
|
@@ -17,10 +17,10 @@ const maestro_signals_1 = require("../workflows/maestro-signals");
|
|
|
17
17
|
const helpers_1 = require("./helpers");
|
|
18
18
|
const validation_1 = require("../utils/validation");
|
|
19
19
|
function registerCoatCheckPutTool(server, client, config, getPlayerId) {
|
|
20
|
-
(0, helpers_1.defineTool)(server, 'coat_check_put', `Stash a large content body on this ensemble's coat-check (#318). Returns a ticket id any player can redeem later via \`coat_check_get\`. Pass the ticket on a \`cue\`'s \`attachmentTicket\` field so the recipient knows what to fetch.
|
|
21
|
-
|
|
22
|
-
Use this when your message body would otherwise exceed the cue's 100 KB cap — researcher reports, review-item dumps, etc. The cue body should carry a short summary; the coat-check entry holds the full artifact.
|
|
23
|
-
|
|
20
|
+
(0, helpers_1.defineTool)(server, 'coat_check_put', `Stash a large content body on this ensemble's coat-check (#318). Returns a ticket id any player can redeem later via \`coat_check_get\`. Pass the ticket on a \`cue\`'s \`attachmentTicket\` field so the recipient knows what to fetch.
|
|
21
|
+
|
|
22
|
+
Use this when your message body would otherwise exceed the cue's 100 KB cap — researcher reports, review-item dumps, etc. The cue body should carry a short summary; the coat-check entry holds the full artifact.
|
|
23
|
+
|
|
24
24
|
Limits: ${validation_1.COAT_CHECK_CONTENT_MAX} bytes (UTF-8) per entry, max ${validation_1.COAT_CHECK_SLOTS_MAX} live entries per ensemble. Saturation rejects with \`CoatCheckSlotsFull\` — wait for TTL or \`coat_check_evict\` an entry you own. TTL defaults to 7 days (configurable per put within [1h, 30d]).`, {
|
|
25
25
|
summary: zod_1.z.string().min(1).max(validation_1.COAT_CHECK_SUMMARY_MAX).describe(`Short preamble surfaced in \`coat_check_list\` and on dashboards (≤${validation_1.COAT_CHECK_SUMMARY_MAX} chars). 1-2 sentences describing what the recipient gets if they redeem.`),
|
|
26
26
|
content: zod_1.z.string().min(1).max(validation_1.COAT_CHECK_CONTENT_MAX).describe(`The full content body — markdown encouraged, opaque to the system. Max ${validation_1.COAT_CHECK_CONTENT_MAX} bytes (UTF-8).`),
|
|
@@ -38,8 +38,8 @@ const validation_1 = require("../utils/validation");
|
|
|
38
38
|
* telemetry shows real demand.
|
|
39
39
|
*/
|
|
40
40
|
function registerFetchStateTool(server, client, config, handle, getPlayerId) {
|
|
41
|
-
(0, helpers_1.defineTool)(server, 'fetch_state', `Read a saved-state slot for yourself or a peer. Defaults to your own "${validation_1.PLAYER_STATE_DEFAULT_KEY}" slot.
|
|
42
|
-
|
|
41
|
+
(0, helpers_1.defineTool)(server, 'fetch_state', `Read a saved-state slot for yourself or a peer. Defaults to your own "${validation_1.PLAYER_STATE_DEFAULT_KEY}" slot.
|
|
42
|
+
|
|
43
43
|
Pass \`playerId\` to read a peer's slot (any player in the ensemble can read any other player's state — audit identity is recorded on each slot via \`savedBy\`). Returns a "(no state saved …)" message when the slot is empty.`, {
|
|
44
44
|
key: zod_1.z.string().regex(validation_1.PLAYER_STATE_KEY_REGEX).max(validation_1.PLAYER_STATE_KEY_MAX).optional().describe(`Slot name (default "${validation_1.PLAYER_STATE_DEFAULT_KEY}").`),
|
|
45
45
|
playerId: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Target player name (default: self).'),
|
package/dist/tools/save-state.js
CHANGED
|
@@ -19,19 +19,19 @@ const signals_1 = require("../workflows/signals");
|
|
|
19
19
|
const helpers_1 = require("./helpers");
|
|
20
20
|
const validation_1 = require("../utils/validation");
|
|
21
21
|
function registerSaveStateTool(server, handle, getPlayerId) {
|
|
22
|
-
(0, helpers_1.defineTool)(server, 'save_state', `Save curated state for yourself into a named slot — a peer can later read it via \`fetch_state\`, and a future restart can seed itself from this artifact instead of replaying the transcript.
|
|
23
|
-
|
|
24
|
-
Recommended structure (markdown, not enforced):
|
|
25
|
-
|
|
26
|
-
## Current task
|
|
27
|
-
...
|
|
28
|
-
## Findings
|
|
29
|
-
...
|
|
30
|
-
## Next steps
|
|
31
|
-
...
|
|
32
|
-
## Open questions
|
|
33
|
-
...
|
|
34
|
-
|
|
22
|
+
(0, helpers_1.defineTool)(server, 'save_state', `Save curated state for yourself into a named slot — a peer can later read it via \`fetch_state\`, and a future restart can seed itself from this artifact instead of replaying the transcript.
|
|
23
|
+
|
|
24
|
+
Recommended structure (markdown, not enforced):
|
|
25
|
+
|
|
26
|
+
## Current task
|
|
27
|
+
...
|
|
28
|
+
## Findings
|
|
29
|
+
...
|
|
30
|
+
## Next steps
|
|
31
|
+
...
|
|
32
|
+
## Open questions
|
|
33
|
+
...
|
|
34
|
+
|
|
35
35
|
Limits: ${validation_1.PLAYER_STATE_CONTENT_MAX} bytes per slot, max ${validation_1.PLAYER_STATE_SLOTS_MAX} slots per player. Slot key defaults to "${validation_1.PLAYER_STATE_DEFAULT_KEY}". When all ${validation_1.PLAYER_STATE_SLOTS_MAX} slots are full, saving a new key fails with \`PlayerStateSlotsFull\` — call \`clear_state\` to free a slot.`, {
|
|
36
36
|
content: zod_1.z.string().min(1).max(validation_1.PLAYER_STATE_CONTENT_MAX).describe(`The state content — markdown encouraged, opaque to the system. Max ${validation_1.PLAYER_STATE_CONTENT_MAX} bytes (UTF-8).`),
|
|
37
37
|
key: zod_1.z.string().regex(validation_1.PLAYER_STATE_KEY_REGEX).max(validation_1.PLAYER_STATE_KEY_MAX).optional().describe(`Slot name (default "${validation_1.PLAYER_STATE_DEFAULT_KEY}"). Alphanumeric + underscore + hyphen, max ${validation_1.PLAYER_STATE_KEY_MAX} chars.`),
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level guard for the Temporal/grpc-js "Channel has been shut down"
|
|
3
|
+
* shutdown race.
|
|
4
|
+
*
|
|
5
|
+
* `@temporalio/client`'s gRPC retry interceptor (`grpc-retry.js`) schedules
|
|
6
|
+
* call retries with `setTimeout`. `Connection.close()` shuts down the
|
|
7
|
+
* underlying grpc-js channel but does NOT clear those pending timers. When a
|
|
8
|
+
* retry timer fires *after* the channel is closed, `InternalChannel.createCall`
|
|
9
|
+
* throws `Error: Channel has been shut down` **synchronously inside the timer
|
|
10
|
+
* callback** — off any awaited path, so no surrounding `try/catch` (including
|
|
11
|
+
* the one around `connection.close()`) can catch it. It escapes to
|
|
12
|
+
* `uncaughtException` and kills the process.
|
|
13
|
+
*
|
|
14
|
+
* Observed stack (the crash this guards against):
|
|
15
|
+
* InternalChannel.createCall (@grpc/grpc-js/.../internal-channel.js)
|
|
16
|
+
* ...
|
|
17
|
+
* Timeout.retry [as _onTimeout] (@temporalio/client/lib/grpc-retry.js)
|
|
18
|
+
*
|
|
19
|
+
* This artifact is always benign: it only occurs for a connection we have
|
|
20
|
+
* already finished with (its query result was captured, or we degraded), as
|
|
21
|
+
* the channel tears down. Swallowing exactly this error — and nothing else —
|
|
22
|
+
* removes the crash without masking real failures. Any other uncaught
|
|
23
|
+
* exception is re-thrown, which Node treats as fatal (print + non-zero exit),
|
|
24
|
+
* preserving normal crash semantics.
|
|
25
|
+
*
|
|
26
|
+
* Install once at CLI entry. Idempotent.
|
|
27
|
+
*
|
|
28
|
+
* Coupling notes:
|
|
29
|
+
* - The match string is grpc-js's exact `close()` error
|
|
30
|
+
* (`@grpc/grpc-js/build/src/internal-channel.js`, the `SHUTDOWN`-state throw).
|
|
31
|
+
* That state is reachable ONLY via an explicit `close()` — a live connection
|
|
32
|
+
* hitting a transient error reports `TRANSIENT_FAILURE`, never this message —
|
|
33
|
+
* so swallowing it cannot mask a failure we'd want to surface. If grpc-js ever
|
|
34
|
+
* renames the message this guard silently becomes a no-op (crash resurfaces);
|
|
35
|
+
* update `CHANNEL_SHUTDOWN_MESSAGE` to match.
|
|
36
|
+
* - This handler is registered first (it's installed at process entry). When it
|
|
37
|
+
* re-throws a non-benign error, Node exits immediately and any LATER
|
|
38
|
+
* `uncaughtException` listener is bypassed. The codebase currently registers
|
|
39
|
+
* no other `uncaughtException` listeners, so this is benign today — revisit if
|
|
40
|
+
* a crash reporter is ever added.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Register the guard on `process`. Safe to call multiple times — only the
|
|
44
|
+
* first call attaches a listener.
|
|
45
|
+
*/
|
|
46
|
+
export declare function installGrpcShutdownGuard(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Test-only escape hatch — removes the listener and resets the install latch so
|
|
49
|
+
* a fresh `installGrpcShutdownGuard()` can be exercised. Never call from
|
|
50
|
+
* production code. See docs/adr/0006-test-hooks-naming.md.
|
|
51
|
+
*/
|
|
52
|
+
export declare function __resetGrpcShutdownGuardForTests(): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Process-level guard for the Temporal/grpc-js "Channel has been shut down"
|
|
4
|
+
* shutdown race.
|
|
5
|
+
*
|
|
6
|
+
* `@temporalio/client`'s gRPC retry interceptor (`grpc-retry.js`) schedules
|
|
7
|
+
* call retries with `setTimeout`. `Connection.close()` shuts down the
|
|
8
|
+
* underlying grpc-js channel but does NOT clear those pending timers. When a
|
|
9
|
+
* retry timer fires *after* the channel is closed, `InternalChannel.createCall`
|
|
10
|
+
* throws `Error: Channel has been shut down` **synchronously inside the timer
|
|
11
|
+
* callback** — off any awaited path, so no surrounding `try/catch` (including
|
|
12
|
+
* the one around `connection.close()`) can catch it. It escapes to
|
|
13
|
+
* `uncaughtException` and kills the process.
|
|
14
|
+
*
|
|
15
|
+
* Observed stack (the crash this guards against):
|
|
16
|
+
* InternalChannel.createCall (@grpc/grpc-js/.../internal-channel.js)
|
|
17
|
+
* ...
|
|
18
|
+
* Timeout.retry [as _onTimeout] (@temporalio/client/lib/grpc-retry.js)
|
|
19
|
+
*
|
|
20
|
+
* This artifact is always benign: it only occurs for a connection we have
|
|
21
|
+
* already finished with (its query result was captured, or we degraded), as
|
|
22
|
+
* the channel tears down. Swallowing exactly this error — and nothing else —
|
|
23
|
+
* removes the crash without masking real failures. Any other uncaught
|
|
24
|
+
* exception is re-thrown, which Node treats as fatal (print + non-zero exit),
|
|
25
|
+
* preserving normal crash semantics.
|
|
26
|
+
*
|
|
27
|
+
* Install once at CLI entry. Idempotent.
|
|
28
|
+
*
|
|
29
|
+
* Coupling notes:
|
|
30
|
+
* - The match string is grpc-js's exact `close()` error
|
|
31
|
+
* (`@grpc/grpc-js/build/src/internal-channel.js`, the `SHUTDOWN`-state throw).
|
|
32
|
+
* That state is reachable ONLY via an explicit `close()` — a live connection
|
|
33
|
+
* hitting a transient error reports `TRANSIENT_FAILURE`, never this message —
|
|
34
|
+
* so swallowing it cannot mask a failure we'd want to surface. If grpc-js ever
|
|
35
|
+
* renames the message this guard silently becomes a no-op (crash resurfaces);
|
|
36
|
+
* update `CHANNEL_SHUTDOWN_MESSAGE` to match.
|
|
37
|
+
* - This handler is registered first (it's installed at process entry). When it
|
|
38
|
+
* re-throws a non-benign error, Node exits immediately and any LATER
|
|
39
|
+
* `uncaughtException` listener is bypassed. The codebase currently registers
|
|
40
|
+
* no other `uncaughtException` listeners, so this is benign today — revisit if
|
|
41
|
+
* a crash reporter is ever added.
|
|
42
|
+
*/
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.installGrpcShutdownGuard = installGrpcShutdownGuard;
|
|
45
|
+
exports.__resetGrpcShutdownGuardForTests = __resetGrpcShutdownGuardForTests;
|
|
46
|
+
const CHANNEL_SHUTDOWN_MESSAGE = 'Channel has been shut down';
|
|
47
|
+
let installed = false;
|
|
48
|
+
function isBenignChannelShutdown(err) {
|
|
49
|
+
return (err instanceof Error &&
|
|
50
|
+
typeof err.message === 'string' &&
|
|
51
|
+
err.message.includes(CHANNEL_SHUTDOWN_MESSAGE));
|
|
52
|
+
}
|
|
53
|
+
const handler = (err) => {
|
|
54
|
+
if (isBenignChannelShutdown(err)) {
|
|
55
|
+
// A Temporal gRPC retry timer fired after we closed the connection. The
|
|
56
|
+
// result we cared about was already captured (or degraded). Drop it.
|
|
57
|
+
if (process.env.CLAUDE_TEMPO_DEBUG) {
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.error('[agent-tempo] ignored post-shutdown gRPC channel error (benign Temporal retry-after-close race)');
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Not ours — restore default crash behavior. Throwing from inside an
|
|
64
|
+
// 'uncaughtException' handler causes Node to print the error and exit with a
|
|
65
|
+
// non-zero code (exit code 7, "Uncaught Exception Handler Error", on current
|
|
66
|
+
// Node — not 1) without re-entering this handler, preserving the original
|
|
67
|
+
// failure's visibility and crash semantics.
|
|
68
|
+
throw err;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Register the guard on `process`. Safe to call multiple times — only the
|
|
72
|
+
* first call attaches a listener.
|
|
73
|
+
*/
|
|
74
|
+
function installGrpcShutdownGuard() {
|
|
75
|
+
if (installed)
|
|
76
|
+
return;
|
|
77
|
+
installed = true;
|
|
78
|
+
process.on('uncaughtException', handler);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Test-only escape hatch — removes the listener and resets the install latch so
|
|
82
|
+
* a fresh `installGrpcShutdownGuard()` can be exercised. Never call from
|
|
83
|
+
* production code. See docs/adr/0006-test-hooks-naming.md.
|
|
84
|
+
*/
|
|
85
|
+
function __resetGrpcShutdownGuardForTests() {
|
|
86
|
+
process.off('uncaughtException', handler);
|
|
87
|
+
installed = false;
|
|
88
|
+
}
|