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.
Files changed (98) hide show
  1. package/CLAUDE.md +219 -219
  2. package/LICENSE +21 -21
  3. package/README.md +289 -289
  4. package/assets/icon-dark.svg +9 -9
  5. package/assets/icon.svg +9 -9
  6. package/assets/logo-dark.svg +11 -11
  7. package/assets/logo-light.svg +11 -11
  8. package/dashboard/README.md +91 -91
  9. package/dashboard/dist/assets/index-D6Xyje_n.js.map +1 -1
  10. package/dashboard/dist/index.html +19 -19
  11. package/dashboard/package.json +47 -47
  12. package/dist/adapters/copilot/adapter.js +12 -1
  13. package/dist/cli/global-wrapper.d.ts +19 -0
  14. package/dist/cli/global-wrapper.js +169 -0
  15. package/dist/cli/help-text.js +97 -97
  16. package/dist/cli/startup.js +11 -0
  17. package/dist/cli/upgrade-command.js +81 -81
  18. package/dist/cli.js +12 -0
  19. package/dist/daemon.js +5 -0
  20. package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
  21. package/dist/server.js +4 -0
  22. package/dist/spawn.js +12 -12
  23. package/dist/tools/coat-check-evict.js +2 -2
  24. package/dist/tools/coat-check-get.js +2 -2
  25. package/dist/tools/coat-check-put.js +4 -4
  26. package/dist/tools/fetch-state.js +2 -2
  27. package/dist/tools/save-state.js +13 -13
  28. package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
  29. package/dist/utils/grpc-shutdown-guard.js +88 -0
  30. package/examples/agents/tempo-composer.md +56 -56
  31. package/examples/agents/tempo-conductor.md +117 -117
  32. package/examples/agents/tempo-critic.md +73 -73
  33. package/examples/agents/tempo-improv.md +74 -74
  34. package/examples/agents/tempo-liner.md +75 -75
  35. package/examples/agents/tempo-roadie.md +61 -61
  36. package/examples/agents/tempo-soloist.md +71 -71
  37. package/examples/agents/tempo-tuner.md +94 -94
  38. package/examples/ensembles/tempo-big-band.yaml +146 -146
  39. package/examples/ensembles/tempo-dev-team.yaml +58 -58
  40. package/examples/ensembles/tempo-headless-jam.yaml +77 -77
  41. package/examples/ensembles/tempo-jam-session.yaml +41 -41
  42. package/examples/ensembles/tempo-mock-jam.yaml +79 -79
  43. package/examples/ensembles/tempo-review-squad.yaml +32 -32
  44. package/package.json +173 -173
  45. package/packaging/launchd/com.agent.tempo.plist +46 -46
  46. package/packaging/systemd/agent-tempo.service +32 -32
  47. package/packaging/windows/install-task.ps1 +71 -71
  48. package/scenarios/conductor-recruit-mock.yaml +33 -33
  49. package/scenarios/echo-roundtrip.yaml +15 -15
  50. package/scenarios/multi-player-handoff.yaml +38 -38
  51. package/scenarios/recruit-cascade.yaml +38 -38
  52. package/scenarios/two-player-conversation.yaml +33 -33
  53. package/workflow-bundle.js +1 -1
  54. package/dist/activities/claude-stop.d.ts +0 -21
  55. package/dist/activities/claude-stop.js +0 -94
  56. package/dist/channel.d.ts +0 -3
  57. package/dist/channel.js +0 -48
  58. package/dist/copilot-bridge.d.ts +0 -22
  59. package/dist/copilot-bridge.js +0 -565
  60. package/dist/scripts/258-spotcheck.js +0 -303
  61. package/dist/tools/detach.d.ts +0 -4
  62. package/dist/tools/detach.js +0 -45
  63. package/dist/tools/encore.d.ts +0 -4
  64. package/dist/tools/encore.js +0 -31
  65. package/dist/tools/pause-ensemble.d.ts +0 -4
  66. package/dist/tools/pause-ensemble.js +0 -58
  67. package/dist/tools/resume-ensemble.d.ts +0 -4
  68. package/dist/tools/resume-ensemble.js +0 -79
  69. package/dist/tools/stop.d.ts +0 -4
  70. package/dist/tools/stop.js +0 -29
  71. package/dist/tui/client.d.ts +0 -6
  72. package/dist/tui/client.js +0 -9
  73. package/dist/tui/components/ActivityLog.d.ts +0 -16
  74. package/dist/tui/components/ActivityLog.js +0 -36
  75. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  76. package/dist/tui/components/CommandOverlay.js +0 -34
  77. package/dist/tui/components/ConductorChat.d.ts +0 -16
  78. package/dist/tui/components/ConductorChat.js +0 -32
  79. package/dist/tui/components/EnsembleListView.d.ts +0 -14
  80. package/dist/tui/components/EnsembleListView.js +0 -32
  81. package/dist/tui/components/EnsemblePanel.d.ts +0 -12
  82. package/dist/tui/components/EnsemblePanel.js +0 -40
  83. package/dist/tui/components/InputBar.d.ts +0 -13
  84. package/dist/tui/components/InputBar.js +0 -58
  85. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  86. package/dist/tui/components/ScheduleOverlay.js +0 -113
  87. package/dist/tui/components/TopBar.d.ts +0 -12
  88. package/dist/tui/components/TopBar.js +0 -15
  89. package/dist/tui/core-api.d.ts +0 -26
  90. package/dist/tui/core-api.js +0 -67
  91. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
  92. package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
  93. package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
  94. package/dist/tui/hooks/useMaestroPoller.js +0 -36
  95. package/dist/tui/hooks/useSendCommand.d.ts +0 -7
  96. package/dist/tui/hooks/useSendCommand.js +0 -29
  97. package/dist/utils/bg-preflight.d.ts +0 -25
  98. 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).'),
@@ -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
+ }