switchroom 0.13.2 → 0.13.3
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/dist/agent-scheduler/index.js +2 -2
- package/dist/auth-broker/index.js +2 -2
- package/dist/cli/switchroom.js +21 -18
- package/dist/host-control/main.js +2 -2
- package/dist/vault/approvals/kernel-server.js +2 -2
- package/dist/vault/broker/server.js +2 -2
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +42 -10
- package/telegram-plugin/gateway/boot-probes.ts +13 -6
- package/telegram-plugin/gateway/gateway.ts +44 -6
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +5 -1
- package/telegram-plugin/silent-end.ts +56 -0
- package/telegram-plugin/tests/boot-probes.test.ts +26 -2
- package/telegram-plugin/tests/silent-end.test.ts +69 -0
- package/skills/buildkite-agent-infrastructure/SKILL.md +0 -321
- package/skills/buildkite-agent-infrastructure/agents/openai.yaml +0 -6
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-infrastructure/references/audit-logging.md +0 -87
- package/skills/buildkite-agent-infrastructure/references/graphql-mutations.md +0 -690
- package/skills/buildkite-agent-infrastructure/references/instance-shapes.md +0 -38
- package/skills/buildkite-agent-infrastructure/references/pipeline-templates.md +0 -73
- package/skills/buildkite-agent-infrastructure/references/self-hosted-agents.md +0 -137
- package/skills/buildkite-agent-infrastructure/references/sso-saml.md +0 -92
- package/skills/buildkite-agent-runtime/SKILL.md +0 -509
- package/skills/buildkite-agent-runtime/agents/openai.yaml +0 -6
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-runtime/references/flag-reference.md +0 -417
- package/skills/buildkite-agent-runtime/references/patterns-and-recipes.md +0 -555
- package/skills/buildkite-api/SKILL.md +0 -308
- package/skills/buildkite-api/agents/openai.yaml +0 -6
- package/skills/buildkite-api/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-api/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-api/references/graphql-reference.md +0 -195
- package/skills/buildkite-api/references/patterns.md +0 -44
- package/skills/buildkite-api/references/webhooks.md +0 -161
- package/skills/buildkite-cli/SKILL.md +0 -397
- package/skills/buildkite-cli/agents/openai.yaml +0 -6
- package/skills/buildkite-cli/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-cli/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-cli/references/command-reference.md +0 -181
- package/skills/buildkite-migration/SKILL.md +0 -195
- package/skills/buildkite-pipelines/SKILL.md +0 -481
- package/skills/buildkite-pipelines/agents/openai.yaml +0 -6
- package/skills/buildkite-pipelines/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-pipelines/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-pipelines/examples/basic-pipeline.yml +0 -24
- package/skills/buildkite-pipelines/examples/optimized-pipeline.yml +0 -100
- package/skills/buildkite-pipelines/references/advanced-patterns.md +0 -286
- package/skills/buildkite-pipelines/references/retry-and-error-codes.md +0 -131
- package/skills/buildkite-pipelines/references/step-types-reference.md +0 -225
- package/skills/buildkite-secure-delivery/SKILL.md +0 -182
- package/skills/buildkite-secure-delivery/agents/openai.yaml +0 -6
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-secure-delivery/references/oidc-cloud-providers.md +0 -83
- package/skills/buildkite-secure-delivery/references/package-publishing.md +0 -100
- package/skills/buildkite-test-engine/SKILL.md +0 -256
- package/skills/buildkite-test-engine/agents/openai.yaml +0 -6
- package/skills/buildkite-test-engine/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-test-engine/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-test-engine/examples/bktec-splitting.yml +0 -16
- package/skills/buildkite-test-engine/examples/collector-pipeline.yml +0 -11
- package/skills/buildkite-test-engine/references/collectors.md +0 -198
- package/skills/buildkite-test-engine/references/splitting-examples.md +0 -93
|
@@ -11130,7 +11130,7 @@ var profileFields = {
|
|
|
11130
11130
|
extends: exports_external.string().optional(),
|
|
11131
11131
|
bot_token: exports_external.string().optional(),
|
|
11132
11132
|
release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) — mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
|
|
11133
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the
|
|
11133
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
|
|
11134
11134
|
soul: exports_external.object({
|
|
11135
11135
|
name: exports_external.string().optional(),
|
|
11136
11136
|
style: exports_external.string().optional(),
|
|
@@ -11191,7 +11191,7 @@ var AgentSchema = exports_external.object({
|
|
|
11191
11191
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
11192
11192
|
release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) — a pinned agent does " + "not inherit the fleet channel, and vice versa."),
|
|
11193
11193
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
11194
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "
|
|
11194
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
11195
11195
|
auth: exports_external.object({
|
|
11196
11196
|
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
11197
11197
|
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
@@ -11130,7 +11130,7 @@ var profileFields = {
|
|
|
11130
11130
|
extends: exports_external.string().optional(),
|
|
11131
11131
|
bot_token: exports_external.string().optional(),
|
|
11132
11132
|
release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) — mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
|
|
11133
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the
|
|
11133
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
|
|
11134
11134
|
soul: exports_external.object({
|
|
11135
11135
|
name: exports_external.string().optional(),
|
|
11136
11136
|
style: exports_external.string().optional(),
|
|
@@ -11191,7 +11191,7 @@ var AgentSchema = exports_external.object({
|
|
|
11191
11191
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
11192
11192
|
release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) — a pinned agent does " + "not inherit the fleet channel, and vice versa."),
|
|
11193
11193
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
11194
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "
|
|
11194
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
11195
11195
|
auth: exports_external.object({
|
|
11196
11196
|
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
11197
11197
|
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13694,7 +13694,7 @@ var init_schema = __esm(() => {
|
|
|
13694
13694
|
extends: exports_external.string().optional(),
|
|
13695
13695
|
bot_token: exports_external.string().optional(),
|
|
13696
13696
|
release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) \u2014 mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
|
|
13697
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the
|
|
13697
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
|
|
13698
13698
|
soul: exports_external.object({
|
|
13699
13699
|
name: exports_external.string().optional(),
|
|
13700
13700
|
style: exports_external.string().optional(),
|
|
@@ -13755,7 +13755,7 @@ var init_schema = __esm(() => {
|
|
|
13755
13755
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
13756
13756
|
release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) \u2014 a pinned agent does " + "not inherit the fleet channel, and vice versa."),
|
|
13757
13757
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
13758
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "
|
|
13758
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
13759
13759
|
auth: exports_external.object({
|
|
13760
13760
|
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only \u2014 " + "this agent talks to the named account regardless of fleet active. See RFC H \u00a74.5.")
|
|
13761
13761
|
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
@@ -47248,8 +47248,8 @@ var {
|
|
|
47248
47248
|
} = import__.default;
|
|
47249
47249
|
|
|
47250
47250
|
// src/build-info.ts
|
|
47251
|
-
var VERSION = "0.13.
|
|
47252
|
-
var COMMIT_SHA = "
|
|
47251
|
+
var VERSION = "0.13.3";
|
|
47252
|
+
var COMMIT_SHA = "e357d33b";
|
|
47253
47253
|
|
|
47254
47254
|
// src/cli/agent.ts
|
|
47255
47255
|
init_source();
|
|
@@ -50658,18 +50658,22 @@ async function summarize(opts) {
|
|
|
50658
50658
|
await mirrorToHindsight(parsed.briefing, opts).catch(() => {});
|
|
50659
50659
|
return "ok";
|
|
50660
50660
|
}
|
|
50661
|
+
function buildHandoffClaudeArgs(opts) {
|
|
50662
|
+
return [
|
|
50663
|
+
"-p",
|
|
50664
|
+
opts.user,
|
|
50665
|
+
"--model",
|
|
50666
|
+
opts.model,
|
|
50667
|
+
"--append-system-prompt",
|
|
50668
|
+
opts.system,
|
|
50669
|
+
"--no-session-persistence",
|
|
50670
|
+
"--strict-mcp-config"
|
|
50671
|
+
];
|
|
50672
|
+
}
|
|
50661
50673
|
var defaultClaudeCliRunner = {
|
|
50662
50674
|
async run({ model, system, user, timeoutMs }) {
|
|
50663
50675
|
return await new Promise((resolve14, reject) => {
|
|
50664
|
-
const args =
|
|
50665
|
-
"-p",
|
|
50666
|
-
user,
|
|
50667
|
-
"--model",
|
|
50668
|
-
model,
|
|
50669
|
-
"--append-system-prompt",
|
|
50670
|
-
system,
|
|
50671
|
-
"--no-session-persistence"
|
|
50672
|
-
];
|
|
50676
|
+
const args = buildHandoffClaudeArgs({ model, system, user });
|
|
50673
50677
|
const env2 = {
|
|
50674
50678
|
...process.env,
|
|
50675
50679
|
FORCE_COLOR: "0",
|
|
@@ -53330,7 +53334,6 @@ Scaffolding agent: ${name}
|
|
|
53330
53334
|
}
|
|
53331
53335
|
try {
|
|
53332
53336
|
const result = reconcileAgent(n, agentConfig, agentsDir, config.telegram, config, configPath, { preserveClaudeMd: opts.preserveClaudeMd });
|
|
53333
|
-
const unitChanges = [];
|
|
53334
53337
|
const allChanges = [...result.changes];
|
|
53335
53338
|
if (allChanges.length === 0) {
|
|
53336
53339
|
console.log(source_default.gray(` ${n}: already in sync`));
|
|
@@ -53341,7 +53344,7 @@ Scaffolding agent: ${name}
|
|
|
53341
53344
|
const semantics = result.changesBySemantics;
|
|
53342
53345
|
if (semantics) {
|
|
53343
53346
|
const { hot, staleTillRestart } = semantics;
|
|
53344
|
-
const restartRequired = [...semantics.restartRequired
|
|
53347
|
+
const restartRequired = [...semantics.restartRequired];
|
|
53345
53348
|
if (restartRequired.length > 0) {
|
|
53346
53349
|
console.log(source_default.yellow(`
|
|
53347
53350
|
Changed (restart required \u2014 soul/MCP/settings/start.sh):`));
|
|
@@ -53384,7 +53387,7 @@ Restart: switchroom agent restart ${n}`));
|
|
|
53384
53387
|
}
|
|
53385
53388
|
}
|
|
53386
53389
|
const changesBySemantics = result.changesBySemantics;
|
|
53387
|
-
const autoRestartNeeded = !opts.noRestart &&
|
|
53390
|
+
const autoRestartNeeded = !opts.noRestart && changesBySemantics !== undefined && changesBySemantics.restartRequired.length > 0;
|
|
53388
53391
|
const shouldRestart = (opts.restart || opts.gracefulRestart || autoRestartNeeded) && allChanges.length > 0;
|
|
53389
53392
|
if (shouldRestart) {
|
|
53390
53393
|
try {
|
|
@@ -53815,7 +53818,7 @@ switchroom agent add: ${name}
|
|
|
53815
53818
|
process.exit(1);
|
|
53816
53819
|
}
|
|
53817
53820
|
}));
|
|
53818
|
-
agent.command("rename <old> <new>").description("Rename an agent slug: stop, snapshot, rename dir +
|
|
53821
|
+
agent.command("rename <old> <new>").description("Rename an agent slug: stop, snapshot, rename dir + vault key, " + "update switchroom.yaml, reconcile, start. Rolls back on any failure.").option("--hindsight <mode>", 'Hindsight bank handling: "preserve" (default, keep old bank) or "fresh" (drop, recreate on first run). "migrate" is deferred.', "preserve").option("-y, --yes", "Skip confirmation prompt").action(withConfigError(async (oldName, newName, opts) => {
|
|
53819
53822
|
const configPath = getConfigPath(program3);
|
|
53820
53823
|
const config = getConfig(program3);
|
|
53821
53824
|
if (!config.agents[oldName]) {
|
|
@@ -53834,7 +53837,7 @@ switchroom agent add: ${name}
|
|
|
53834
53837
|
if (!opts.yes) {
|
|
53835
53838
|
process.stdout.write(source_default.yellow(`
|
|
53836
53839
|
Rename agent "${oldName}" \u2192 "${newName}"?
|
|
53837
|
-
` + ` This will: stop ${oldName}, copy dir,
|
|
53840
|
+
` + ` This will: stop ${oldName}, copy dir,
|
|
53838
53841
|
` + ` rename vault keys (if passphrase available), update switchroom.yaml,
|
|
53839
53842
|
` + ` reconcile, and start ${newName}.
|
|
53840
53843
|
` + ` Hindsight mode: ${hindsightMode}
|
|
@@ -13863,7 +13863,7 @@ var profileFields = {
|
|
|
13863
13863
|
extends: exports_external.string().optional(),
|
|
13864
13864
|
bot_token: exports_external.string().optional(),
|
|
13865
13865
|
release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) — mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
|
|
13866
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the
|
|
13866
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
|
|
13867
13867
|
soul: exports_external.object({
|
|
13868
13868
|
name: exports_external.string().optional(),
|
|
13869
13869
|
style: exports_external.string().optional(),
|
|
@@ -13924,7 +13924,7 @@ var AgentSchema = exports_external.object({
|
|
|
13924
13924
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
13925
13925
|
release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) — a pinned agent does " + "not inherit the fleet channel, and vice versa."),
|
|
13926
13926
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
13927
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "
|
|
13927
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
13928
13928
|
auth: exports_external.object({
|
|
13929
13929
|
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
13930
13930
|
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
@@ -11122,7 +11122,7 @@ var init_schema = __esm(() => {
|
|
|
11122
11122
|
extends: exports_external.string().optional(),
|
|
11123
11123
|
bot_token: exports_external.string().optional(),
|
|
11124
11124
|
release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) — mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
|
|
11125
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the
|
|
11125
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
|
|
11126
11126
|
soul: exports_external.object({
|
|
11127
11127
|
name: exports_external.string().optional(),
|
|
11128
11128
|
style: exports_external.string().optional(),
|
|
@@ -11183,7 +11183,7 @@ var init_schema = __esm(() => {
|
|
|
11183
11183
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
11184
11184
|
release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) — a pinned agent does " + "not inherit the fleet channel, and vice versa."),
|
|
11185
11185
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
11186
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "
|
|
11186
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
11187
11187
|
auth: exports_external.object({
|
|
11188
11188
|
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
11189
11189
|
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
@@ -11122,7 +11122,7 @@ var init_schema = __esm(() => {
|
|
|
11122
11122
|
extends: exports_external.string().optional(),
|
|
11123
11123
|
bot_token: exports_external.string().optional(),
|
|
11124
11124
|
release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) — mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
|
|
11125
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the
|
|
11125
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
|
|
11126
11126
|
soul: exports_external.object({
|
|
11127
11127
|
name: exports_external.string().optional(),
|
|
11128
11128
|
style: exports_external.string().optional(),
|
|
@@ -11183,7 +11183,7 @@ var init_schema = __esm(() => {
|
|
|
11183
11183
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
11184
11184
|
release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) — a pinned agent does " + "not inherit the fleet channel, and vice versa."),
|
|
11185
11185
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
11186
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "
|
|
11186
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
11187
11187
|
auth: exports_external.object({
|
|
11188
11188
|
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
|
|
11189
11189
|
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
package/package.json
CHANGED
|
@@ -23766,7 +23766,7 @@ var init_schema = __esm(() => {
|
|
|
23766
23766
|
extends: exports_external.string().optional(),
|
|
23767
23767
|
bot_token: exports_external.string().optional(),
|
|
23768
23768
|
release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) \u2014 mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
|
|
23769
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the
|
|
23769
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
|
|
23770
23770
|
soul: exports_external.object({
|
|
23771
23771
|
name: exports_external.string().optional(),
|
|
23772
23772
|
style: exports_external.string().optional(),
|
|
@@ -23827,7 +23827,7 @@ var init_schema = __esm(() => {
|
|
|
23827
23827
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
23828
23828
|
release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) \u2014 a pinned agent does " + "not inherit the fleet channel, and vice versa."),
|
|
23829
23829
|
bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
|
|
23830
|
-
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "
|
|
23830
|
+
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
|
|
23831
23831
|
auth: exports_external.object({
|
|
23832
23832
|
override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only \u2014 " + "this agent talks to the named account regardless of fleet active. See RFC H \u00a74.5.")
|
|
23833
23833
|
}).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
|
|
@@ -27542,16 +27542,17 @@ function uptimeMsForStarttime(starttimeTicks, fs2 = realProcFs) {
|
|
|
27542
27542
|
}
|
|
27543
27543
|
}
|
|
27544
27544
|
function nextStepForAgentState(agentName3, state4) {
|
|
27545
|
+
const tailCmd = process.env.SWITCHROOM_RUNTIME === "docker" ? `docker logs --tail 100 switchroom-${agentName3}` : `journalctl --user -u switchroom-${agentName3} -n 100`;
|
|
27545
27546
|
if (state4 === "failed") {
|
|
27546
|
-
return `Service failed \u2014 inspect with
|
|
27547
|
+
return `Service failed \u2014 inspect with \`${tailCmd}\` then \`switchroom agent restart ${agentName3}\``;
|
|
27547
27548
|
}
|
|
27548
27549
|
if (state4 === "inactive") {
|
|
27549
|
-
return `Service inactive \u2014 start with \`switchroom agent start ${agentName3}
|
|
27550
|
+
return `Service inactive \u2014 start with \`switchroom agent start ${agentName3}\``;
|
|
27550
27551
|
}
|
|
27551
27552
|
if (state4 === "deactivating" || state4 === "activating" || state4 === "auto-restart") {
|
|
27552
27553
|
return `Service is in a transient \`${state4}\` state \u2014 re-check with \`switchroom agent status ${agentName3}\` in a few seconds`;
|
|
27553
27554
|
}
|
|
27554
|
-
return `Inspect with
|
|
27555
|
+
return `Inspect with \`${tailCmd}\``;
|
|
27555
27556
|
}
|
|
27556
27557
|
function probeAgentProcessDocker() {
|
|
27557
27558
|
const found = findAgentProcessInContainer();
|
|
@@ -37121,6 +37122,7 @@ function startTimer(deps) {
|
|
|
37121
37122
|
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync5 } from "node:fs";
|
|
37122
37123
|
import { dirname as dirname6, join as join6 } from "node:path";
|
|
37123
37124
|
import { homedir as homedir2 } from "node:os";
|
|
37125
|
+
var SILENT_END_MAX_RETRIES = 1;
|
|
37124
37126
|
function resolveStateDir(deps) {
|
|
37125
37127
|
if (deps?.stateDir != null)
|
|
37126
37128
|
return deps.stateDir;
|
|
@@ -37182,6 +37184,27 @@ function clearSilentEndState(turnKey, deps) {
|
|
|
37182
37184
|
`);
|
|
37183
37185
|
} catch {}
|
|
37184
37186
|
}
|
|
37187
|
+
function readSilentEndState(deps) {
|
|
37188
|
+
const statePath = resolveStatePath(deps);
|
|
37189
|
+
if (!existsSync5(statePath))
|
|
37190
|
+
return null;
|
|
37191
|
+
try {
|
|
37192
|
+
return JSON.parse(readFileSync3(statePath, "utf8"));
|
|
37193
|
+
} catch {
|
|
37194
|
+
return null;
|
|
37195
|
+
}
|
|
37196
|
+
}
|
|
37197
|
+
function recordSilentTurnEnd(args, deps) {
|
|
37198
|
+
const prev = readSilentEndState(deps);
|
|
37199
|
+
if (prev != null && prev.turnKey === args.turnKey && prev.retryCount >= SILENT_END_MAX_RETRIES) {
|
|
37200
|
+
clearSilentEndState(args.turnKey, deps);
|
|
37201
|
+
emitLog(deps, `silent-end: re-prompt exhausted for turnKey=${args.turnKey} ` + `(retryCount=${prev.retryCount} >= ${SILENT_END_MAX_RETRIES}) \u2014 ` + `caller should deliver a fallback
|
|
37202
|
+
`);
|
|
37203
|
+
return { exhausted: true };
|
|
37204
|
+
}
|
|
37205
|
+
writeSilentEndState(args, deps);
|
|
37206
|
+
return { exhausted: false };
|
|
37207
|
+
}
|
|
37185
37208
|
|
|
37186
37209
|
// turn-flush-safety.ts
|
|
37187
37210
|
var SILENT_MARKERS = new Set(["NO_REPLY", "HEARTBEAT_OK"]);
|
|
@@ -47679,10 +47702,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
47679
47702
|
}
|
|
47680
47703
|
|
|
47681
47704
|
// ../src/build-info.ts
|
|
47682
|
-
var VERSION = "0.13.
|
|
47683
|
-
var COMMIT_SHA = "
|
|
47684
|
-
var COMMIT_DATE = "2026-05-
|
|
47685
|
-
var LATEST_PR =
|
|
47705
|
+
var VERSION = "0.13.3";
|
|
47706
|
+
var COMMIT_SHA = "e357d33b";
|
|
47707
|
+
var COMMIT_DATE = "2026-05-21T07:05:47Z";
|
|
47708
|
+
var LATEST_PR = 1621;
|
|
47686
47709
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
47687
47710
|
|
|
47688
47711
|
// gateway/boot-version.ts
|
|
@@ -48170,6 +48193,7 @@ function resolveCallingSubagent(opts) {
|
|
|
48170
48193
|
|
|
48171
48194
|
// gateway/gateway.ts
|
|
48172
48195
|
var REPLY_TO_TEXT_MAX = 200;
|
|
48196
|
+
var SILENT_END_FALLBACK_TEXT = "\u26A0\uFE0F The agent finished working but didn\u2019t send a reply \u2014 your last " + "message may not have been answered. Please try asking again.";
|
|
48173
48197
|
installStderrTimestamps();
|
|
48174
48198
|
installPluginLogger();
|
|
48175
48199
|
installGlobalErrorHandlers();
|
|
@@ -51610,11 +51634,19 @@ function handleSessionEvent(ev) {
|
|
|
51610
51634
|
ended_via: outboundMetrics.outboundCount > 0 ? "reply" : "silent"
|
|
51611
51635
|
});
|
|
51612
51636
|
if (outboundMetrics.outboundCount === 0) {
|
|
51613
|
-
|
|
51637
|
+
const silentEnd = recordSilentTurnEnd({
|
|
51614
51638
|
chatId,
|
|
51615
51639
|
threadId: threadId ?? null,
|
|
51616
51640
|
turnKey: tKey
|
|
51617
51641
|
});
|
|
51642
|
+
if (silentEnd.exhausted) {
|
|
51643
|
+
process.stderr.write(`telegram gateway: WARN silent-end fallback \u2014 agent stayed ` + `silent after the Stop-hook re-prompt; delivering fallback message chat=${chatId} turnKey=${tKey} (#1161)
|
|
51644
|
+
`);
|
|
51645
|
+
retryWithThreadFallback(robustApiCall, (tid) => bot.api.sendMessage(chatId, SILENT_END_FALLBACK_TEXT, tid != null ? { message_thread_id: tid } : {}), { threadId, chat_id: chatId, verb: "silent-end-fallback.sendMessage" }).catch((err) => {
|
|
51646
|
+
process.stderr.write(`telegram gateway: silent-end fallback send failed: ${err instanceof Error ? err.message : String(err)}
|
|
51647
|
+
`);
|
|
51648
|
+
});
|
|
51649
|
+
}
|
|
51618
51650
|
}
|
|
51619
51651
|
clear(tKey);
|
|
51620
51652
|
endTurn(tKey);
|
|
@@ -422,24 +422,31 @@ export function uptimeMsForStarttime(
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
/**
|
|
425
|
-
* Compute a remediation hint for a non-active agent
|
|
425
|
+
* Compute a remediation hint for a non-active agent state. Returns
|
|
426
426
|
* `undefined` when no actionable hint applies. Per `reference/principles.md`
|
|
427
427
|
* principle 1, every degraded/fail row should tell the user what to do next.
|
|
428
|
-
*
|
|
429
|
-
* agents
|
|
428
|
+
*
|
|
429
|
+
* Runtime-aware: v0.7+ agents run in Docker — there is no systemd unit or
|
|
430
|
+
* `journalctl` in-container, so the log-tail command is gated on
|
|
431
|
+
* `SWITCHROOM_RUNTIME` exactly like the boot-card crash row (see
|
|
432
|
+
* `boot-card.ts` and #1376/#1382). All hints share a common log-tail shape
|
|
433
|
+
* so they stay greppable across agents.
|
|
430
434
|
*/
|
|
431
435
|
function nextStepForAgentState(agentName: string, state: string): string | undefined {
|
|
436
|
+
const tailCmd = process.env.SWITCHROOM_RUNTIME === 'docker'
|
|
437
|
+
? `docker logs --tail 100 switchroom-${agentName}`
|
|
438
|
+
: `journalctl --user -u switchroom-${agentName} -n 100`
|
|
432
439
|
if (state === 'failed') {
|
|
433
|
-
return `Service failed — inspect with
|
|
440
|
+
return `Service failed — inspect with \`${tailCmd}\` then \`switchroom agent restart ${agentName}\``
|
|
434
441
|
}
|
|
435
442
|
if (state === 'inactive') {
|
|
436
|
-
return `Service inactive — start with \`switchroom agent start ${agentName}
|
|
443
|
+
return `Service inactive — start with \`switchroom agent start ${agentName}\``
|
|
437
444
|
}
|
|
438
445
|
if (state === 'deactivating' || state === 'activating' || state === 'auto-restart') {
|
|
439
446
|
return `Service is in a transient \`${state}\` state — re-check with \`switchroom agent status ${agentName}\` in a few seconds`
|
|
440
447
|
}
|
|
441
448
|
// Unknown state — keep the door open with a generic hint.
|
|
442
|
-
return `Inspect with
|
|
449
|
+
return `Inspect with \`${tailCmd}\``
|
|
443
450
|
}
|
|
444
451
|
|
|
445
452
|
function probeAgentProcessDocker(): ProbeResult {
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
import { emitRuntimeMetric } from '../runtime-metrics.js'
|
|
77
77
|
import { classifyInbound } from '../inbound-classifier.js'
|
|
78
78
|
import * as silencePoke from '../silence-poke.js'
|
|
79
|
-
import { writeSilentEndState, clearSilentEndState } from '../silent-end.js'
|
|
79
|
+
import { writeSilentEndState, clearSilentEndState, recordSilentTurnEnd } from '../silent-end.js'
|
|
80
80
|
import { createAnswerStream, type AnswerStreamHandle } from '../answer-stream.js'
|
|
81
81
|
import { type SessionEvent } from '../session-tail.js'
|
|
82
82
|
import {
|
|
@@ -139,6 +139,16 @@ import { validateStringArray } from './access-validator.js'
|
|
|
139
139
|
* identical envelope shapes.
|
|
140
140
|
*/
|
|
141
141
|
const REPLY_TO_TEXT_MAX = 200
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* #1161 — user-facing fallback delivered when a user-message turn ends
|
|
145
|
+
* with zero outbound messages AND the deterministic Stop-hook re-prompt
|
|
146
|
+
* has already been exhausted. Without this the user only sees the
|
|
147
|
+
* progress card vanish; silence must never be the failure mode.
|
|
148
|
+
*/
|
|
149
|
+
const SILENT_END_FALLBACK_TEXT =
|
|
150
|
+
'⚠️ The agent finished working but didn’t send a reply — your last ' +
|
|
151
|
+
'message may not have been answered. Please try asking again.'
|
|
142
152
|
import { markdownToHtml, splitHtmlChunks, repairEscapedWhitespace, telegramHtmlToPlainText } from '../format.js'
|
|
143
153
|
import {
|
|
144
154
|
validateInlineKeyboard,
|
|
@@ -6290,16 +6300,44 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
6290
6300
|
longest_silent_gap_ms: outboundMetrics.longestOutboundGapMs,
|
|
6291
6301
|
ended_via: outboundMetrics.outboundCount > 0 ? 'reply' : 'silent',
|
|
6292
6302
|
})
|
|
6293
|
-
// #1122 PR4
|
|
6294
|
-
// silent-marker path above for the rationale).
|
|
6295
|
-
//
|
|
6296
|
-
// the
|
|
6303
|
+
// #1122 PR4 / #1161: deterministic silent-end handling (see the
|
|
6304
|
+
// silent-marker path above for the rationale).
|
|
6305
|
+
// - first silent-end → recordSilentTurnEnd writes the state
|
|
6306
|
+
// file so the Stop hook (silent-end-interrupt-stop.mjs)
|
|
6307
|
+
// blocks the session-end and re-prompts the agent to reply.
|
|
6308
|
+
// - the Stop-hook re-prompt is already spent and the agent is
|
|
6309
|
+
// STILL silent → recordSilentTurnEnd returns exhausted:true;
|
|
6310
|
+
// deliver a user-facing fallback so the turn never just
|
|
6311
|
+
// vanishes (the user otherwise only sees the card disappear).
|
|
6297
6312
|
if (outboundMetrics.outboundCount === 0) {
|
|
6298
|
-
|
|
6313
|
+
const silentEnd = recordSilentTurnEnd({
|
|
6299
6314
|
chatId,
|
|
6300
6315
|
threadId: threadId ?? null,
|
|
6301
6316
|
turnKey: tKey,
|
|
6302
6317
|
})
|
|
6318
|
+
if (silentEnd.exhausted) {
|
|
6319
|
+
process.stderr.write(
|
|
6320
|
+
`telegram gateway: WARN silent-end fallback — agent stayed ` +
|
|
6321
|
+
`silent after the Stop-hook re-prompt; delivering fallback ` +
|
|
6322
|
+
`message chat=${chatId} turnKey=${tKey} (#1161)\n`,
|
|
6323
|
+
)
|
|
6324
|
+
void retryWithThreadFallback(
|
|
6325
|
+
robustApiCall,
|
|
6326
|
+
(tid) =>
|
|
6327
|
+
bot.api.sendMessage(
|
|
6328
|
+
chatId,
|
|
6329
|
+
SILENT_END_FALLBACK_TEXT,
|
|
6330
|
+
tid != null ? { message_thread_id: tid } : {},
|
|
6331
|
+
),
|
|
6332
|
+
{ threadId, chat_id: chatId, verb: 'silent-end-fallback.sendMessage' },
|
|
6333
|
+
).catch((err) => {
|
|
6334
|
+
process.stderr.write(
|
|
6335
|
+
`telegram gateway: silent-end fallback send failed: ${
|
|
6336
|
+
err instanceof Error ? err.message : String(err)
|
|
6337
|
+
}\n`,
|
|
6338
|
+
)
|
|
6339
|
+
})
|
|
6340
|
+
}
|
|
6303
6341
|
}
|
|
6304
6342
|
signalTracker.clear(tKey)
|
|
6305
6343
|
silencePoke.endTurn(tKey)
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* decision:block to re-prompt the agent instead of letting the session close.
|
|
10
10
|
*
|
|
11
11
|
* On the second silent-end (retryCount >= MAX_RETRIES), the hook allows the
|
|
12
|
-
* stop
|
|
12
|
+
* stop. The gateway's turn-end path (recordSilentTurnEnd in silent-end.ts)
|
|
13
|
+
* detects the exhausted re-prompt and delivers a user-facing fallback
|
|
14
|
+
* message so the turn never silently vanishes (#1161).
|
|
13
15
|
*
|
|
14
16
|
* Carve-outs preserved:
|
|
15
17
|
* - wasAutonomous=true turns: the gateway never writes a state file for
|
|
@@ -30,6 +32,8 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
|
30
32
|
import { join } from 'node:path'
|
|
31
33
|
import { homedir } from 'node:os'
|
|
32
34
|
|
|
35
|
+
// MUST stay in sync with SILENT_END_MAX_RETRIES in telegram-plugin/silent-end.ts
|
|
36
|
+
// (this hook is a standalone .mjs and can't import the TS module).
|
|
33
37
|
const MAX_RETRIES = 1
|
|
34
38
|
|
|
35
39
|
function readStdin() {
|
|
@@ -51,6 +51,14 @@ export interface SilentEndDeps {
|
|
|
51
51
|
log?: (line: string) => void
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* How many times the Stop hook re-prompts a silent-end turn before it
|
|
56
|
+
* gives up. MUST stay in sync with `MAX_RETRIES` in the Stop hook
|
|
57
|
+
* (`telegram-plugin/hooks/silent-end-interrupt-stop.mjs`) — the hook is a
|
|
58
|
+
* standalone `.mjs` and can't import this module.
|
|
59
|
+
*/
|
|
60
|
+
export const SILENT_END_MAX_RETRIES = 1
|
|
61
|
+
|
|
54
62
|
function resolveStateDir(deps?: SilentEndDeps): string {
|
|
55
63
|
if (deps?.stateDir != null) return deps.stateDir
|
|
56
64
|
const env = process.env.TELEGRAM_STATE_DIR
|
|
@@ -172,3 +180,51 @@ export function readSilentEndState(deps?: SilentEndDeps): SilentEndState | null
|
|
|
172
180
|
return null
|
|
173
181
|
}
|
|
174
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Record a user-message turn that ended with zero outbound messages and
|
|
186
|
+
* report whether the deterministic re-prompt has been exhausted. This is
|
|
187
|
+
* the gateway's single entry point for the main turn-end path.
|
|
188
|
+
*
|
|
189
|
+
* - First silent-end of a turn (no prior state, or prior `retryCount`
|
|
190
|
+
* still below `SILENT_END_MAX_RETRIES`) → writes the state file via
|
|
191
|
+
* `writeSilentEndState`, so `silent-end-interrupt-stop.mjs` blocks
|
|
192
|
+
* the stop and re-prompts the agent. Returns `{ exhausted: false }`.
|
|
193
|
+
*
|
|
194
|
+
* - A silent-end where the prior state for the SAME turn already shows
|
|
195
|
+
* `retryCount >= SILENT_END_MAX_RETRIES` → the Stop hook already
|
|
196
|
+
* spent its re-prompt and the agent is STILL silent. Recovery has
|
|
197
|
+
* failed. Clears the state file (so the Stop hook on this final turn
|
|
198
|
+
* finds nothing pending and allows the stop cleanly) and returns
|
|
199
|
+
* `{ exhausted: true }` — the caller MUST then deliver a user-facing
|
|
200
|
+
* fallback so the turn never just vanishes (#1161).
|
|
201
|
+
*
|
|
202
|
+
* Chat-less autonomous wakeup turns never reach here: the gateway only
|
|
203
|
+
* creates a `currentTurn` (and therefore only runs a turn-end handler)
|
|
204
|
+
* when the inbound event carries a chat id. Cron-fired turns DO carry a
|
|
205
|
+
* topic chat and reach this path — a cron task that means to stay silent
|
|
206
|
+
* must emit a NO_REPLY sentinel, which routes to the gateway's
|
|
207
|
+
* silent-marker branch and never gets a fallback.
|
|
208
|
+
*/
|
|
209
|
+
export function recordSilentTurnEnd(
|
|
210
|
+
args: { chatId: string; threadId: number | null; turnKey: string },
|
|
211
|
+
deps?: SilentEndDeps,
|
|
212
|
+
): { exhausted: boolean } {
|
|
213
|
+
const prev = readSilentEndState(deps)
|
|
214
|
+
if (
|
|
215
|
+
prev != null &&
|
|
216
|
+
prev.turnKey === args.turnKey &&
|
|
217
|
+
prev.retryCount >= SILENT_END_MAX_RETRIES
|
|
218
|
+
) {
|
|
219
|
+
clearSilentEndState(args.turnKey, deps)
|
|
220
|
+
emitLog(
|
|
221
|
+
deps,
|
|
222
|
+
`silent-end: re-prompt exhausted for turnKey=${args.turnKey} ` +
|
|
223
|
+
`(retryCount=${prev.retryCount} >= ${SILENT_END_MAX_RETRIES}) — ` +
|
|
224
|
+
`caller should deliver a fallback\n`,
|
|
225
|
+
)
|
|
226
|
+
return { exhausted: true }
|
|
227
|
+
}
|
|
228
|
+
writeSilentEndState(args, deps)
|
|
229
|
+
return { exhausted: false }
|
|
230
|
+
}
|
|
@@ -1185,8 +1185,15 @@ describe('uptimeMsForStarttime', () => {
|
|
|
1185
1185
|
// the probes covered by the boot-card-dedup-and-next-steps PR so we don't
|
|
1186
1186
|
// silently lose the hint on a future refactor.
|
|
1187
1187
|
|
|
1188
|
-
describe('nextStep — agent
|
|
1189
|
-
|
|
1188
|
+
describe('nextStep — agent states', () => {
|
|
1189
|
+
const savedRuntime = process.env.SWITCHROOM_RUNTIME
|
|
1190
|
+
afterEach(() => {
|
|
1191
|
+
if (savedRuntime === undefined) delete process.env.SWITCHROOM_RUNTIME
|
|
1192
|
+
else process.env.SWITCHROOM_RUNTIME = savedRuntime
|
|
1193
|
+
})
|
|
1194
|
+
|
|
1195
|
+
it('attaches a journalctl hint when the unit is failed (non-docker runtime)', async () => {
|
|
1196
|
+
delete process.env.SWITCHROOM_RUNTIME
|
|
1190
1197
|
const exec = makeSequence([makeSystemctlOutput('failed')])
|
|
1191
1198
|
const r = await probeAgentProcess('klanker', {
|
|
1192
1199
|
execFileImpl: exec as unknown as (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,
|
|
@@ -1199,6 +1206,23 @@ describe('nextStep — agent systemd states', () => {
|
|
|
1199
1206
|
expect(r.nextStep).toMatch(/switchroom-klanker/)
|
|
1200
1207
|
})
|
|
1201
1208
|
|
|
1209
|
+
// #1382: the failed/unknown-state hints must follow SWITCHROOM_RUNTIME the
|
|
1210
|
+
// same way the boot-card crash row does (#1376) — no journalctl in-container.
|
|
1211
|
+
it('attaches a docker-logs hint when the unit is failed under SWITCHROOM_RUNTIME=docker', async () => {
|
|
1212
|
+
process.env.SWITCHROOM_RUNTIME = 'docker'
|
|
1213
|
+
const exec = makeSequence([makeSystemctlOutput('failed')])
|
|
1214
|
+
const r = await probeAgentProcess('klanker', {
|
|
1215
|
+
execFileImpl: exec as unknown as (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,
|
|
1216
|
+
sleepImpl: async () => {},
|
|
1217
|
+
retryIntervalMs: 1,
|
|
1218
|
+
retryMaxMs: 0,
|
|
1219
|
+
})
|
|
1220
|
+
expect(r.status).toBe('fail')
|
|
1221
|
+
expect(r.nextStep).toMatch(/docker logs/)
|
|
1222
|
+
expect(r.nextStep).toMatch(/switchroom-klanker/)
|
|
1223
|
+
expect(r.nextStep).not.toMatch(/journalctl/)
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1202
1226
|
it('attaches a transient-state hint when the unit is activating after retry budget', async () => {
|
|
1203
1227
|
const exec = makeSequence([makeSystemctlOutput('activating')])
|
|
1204
1228
|
const r = await probeAgentProcess('klanker', {
|