switchroom 0.15.13 → 0.15.15
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 +312 -13
- package/dist/auth-broker/index.js +49 -3
- package/dist/cli/notion-write-pretool.mjs +49 -3
- package/dist/cli/switchroom.js +335 -107
- package/dist/host-control/main.js +49 -3
- package/dist/vault/approvals/kernel-server.js +50 -4
- package/dist/vault/broker/server.js +50 -4
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +49 -15
- package/telegram-plugin/bridge/bridge.ts +7 -1
- package/telegram-plugin/dist/bridge/bridge.js +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +108 -9
- package/telegram-plugin/dist/server.js +1 -1
- package/telegram-plugin/gateway/gateway.ts +46 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +31 -1
- package/telegram-plugin/gateway/ipc-server.ts +29 -0
- package/telegram-plugin/runtime-metrics.ts +14 -0
- package/telegram-plugin/tests/bridge-liveness-override.test.ts +21 -0
- package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +54 -0
- package/telegram-plugin/tests/runtime-metrics.test.ts +9 -0
- package/telegram-plugin/tests/send-outbound-wiring.test.ts +63 -0
|
@@ -13724,11 +13724,29 @@ var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
|
13724
13724
|
HttpDiffPollSchema,
|
|
13725
13725
|
TelegramReactionsPollSchema
|
|
13726
13726
|
]);
|
|
13727
|
+
var TelegramMessageActionSchema = exports_external.object({
|
|
13728
|
+
type: exports_external.literal("telegram-message"),
|
|
13729
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
13730
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
13731
|
+
});
|
|
13732
|
+
var WebhookActionSchema = exports_external.object({
|
|
13733
|
+
type: exports_external.literal("webhook"),
|
|
13734
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
13735
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
13736
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
13737
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
13738
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (§6.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
13739
|
+
});
|
|
13740
|
+
var ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
13741
|
+
TelegramMessageActionSchema,
|
|
13742
|
+
WebhookActionSchema
|
|
13743
|
+
]);
|
|
13727
13744
|
var ScheduleEntrySchema = exports_external.object({
|
|
13728
13745
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
13729
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
13730
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
13746
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
13747
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
13731
13748
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
13749
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
13732
13750
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
13733
13751
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' → a minimal-" + "context cheap cron session (Tier 1). 'agent' → the agent's live " + "session with full persona/memory (Tier 2). Unset → inferred from " + "`model` (cheap→fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
13734
13752
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -13745,13 +13763,41 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
13745
13763
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
13746
13764
|
});
|
|
13747
13765
|
}
|
|
13748
|
-
if (kind
|
|
13766
|
+
if (kind !== "poll" && entry.poll) {
|
|
13749
13767
|
ctx.addIssue({
|
|
13750
13768
|
code: exports_external.ZodIssueCode.custom,
|
|
13751
13769
|
path: ["poll"],
|
|
13752
13770
|
message: "`poll` is only valid when kind: poll."
|
|
13753
13771
|
});
|
|
13754
13772
|
}
|
|
13773
|
+
if (kind === "action" && !entry.action) {
|
|
13774
|
+
ctx.addIssue({
|
|
13775
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13776
|
+
path: ["action"],
|
|
13777
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
13778
|
+
});
|
|
13779
|
+
}
|
|
13780
|
+
if (kind !== "action" && entry.action) {
|
|
13781
|
+
ctx.addIssue({
|
|
13782
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13783
|
+
path: ["action"],
|
|
13784
|
+
message: "`action` is only valid when kind: action."
|
|
13785
|
+
});
|
|
13786
|
+
}
|
|
13787
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
13788
|
+
ctx.addIssue({
|
|
13789
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13790
|
+
path: ["prompt"],
|
|
13791
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
13792
|
+
});
|
|
13793
|
+
}
|
|
13794
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
13795
|
+
ctx.addIssue({
|
|
13796
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13797
|
+
path: ["prompt"],
|
|
13798
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
13799
|
+
});
|
|
13800
|
+
}
|
|
13755
13801
|
});
|
|
13756
13802
|
var AgentSoulSchema = exports_external.object({
|
|
13757
13803
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11299,7 +11299,7 @@ var init_dist = __esm(() => {
|
|
|
11299
11299
|
});
|
|
11300
11300
|
|
|
11301
11301
|
// src/config/schema.ts
|
|
11302
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11302
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11303
11303
|
var init_schema = __esm(() => {
|
|
11304
11304
|
init_zod();
|
|
11305
11305
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11332,11 +11332,29 @@ var init_schema = __esm(() => {
|
|
|
11332
11332
|
HttpDiffPollSchema,
|
|
11333
11333
|
TelegramReactionsPollSchema
|
|
11334
11334
|
]);
|
|
11335
|
+
TelegramMessageActionSchema = exports_external.object({
|
|
11336
|
+
type: exports_external.literal("telegram-message"),
|
|
11337
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
11338
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
11339
|
+
});
|
|
11340
|
+
WebhookActionSchema = exports_external.object({
|
|
11341
|
+
type: exports_external.literal("webhook"),
|
|
11342
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
11343
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
11344
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
11345
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
11346
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (§6.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
11347
|
+
});
|
|
11348
|
+
ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11349
|
+
TelegramMessageActionSchema,
|
|
11350
|
+
WebhookActionSchema
|
|
11351
|
+
]);
|
|
11335
11352
|
ScheduleEntrySchema = exports_external.object({
|
|
11336
11353
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11337
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11338
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
11354
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
11355
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
11339
11356
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11357
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
11340
11358
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
11341
11359
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' → a minimal-" + "context cheap cron session (Tier 1). 'agent' → the agent's live " + "session with full persona/memory (Tier 2). Unset → inferred from " + "`model` (cheap→fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
11342
11360
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -11353,13 +11371,41 @@ var init_schema = __esm(() => {
|
|
|
11353
11371
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11354
11372
|
});
|
|
11355
11373
|
}
|
|
11356
|
-
if (kind
|
|
11374
|
+
if (kind !== "poll" && entry.poll) {
|
|
11357
11375
|
ctx.addIssue({
|
|
11358
11376
|
code: exports_external.ZodIssueCode.custom,
|
|
11359
11377
|
path: ["poll"],
|
|
11360
11378
|
message: "`poll` is only valid when kind: poll."
|
|
11361
11379
|
});
|
|
11362
11380
|
}
|
|
11381
|
+
if (kind === "action" && !entry.action) {
|
|
11382
|
+
ctx.addIssue({
|
|
11383
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11384
|
+
path: ["action"],
|
|
11385
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
11386
|
+
});
|
|
11387
|
+
}
|
|
11388
|
+
if (kind !== "action" && entry.action) {
|
|
11389
|
+
ctx.addIssue({
|
|
11390
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11391
|
+
path: ["action"],
|
|
11392
|
+
message: "`action` is only valid when kind: action."
|
|
11393
|
+
});
|
|
11394
|
+
}
|
|
11395
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
11396
|
+
ctx.addIssue({
|
|
11397
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11398
|
+
path: ["prompt"],
|
|
11399
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
11400
|
+
});
|
|
11401
|
+
}
|
|
11402
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
11403
|
+
ctx.addIssue({
|
|
11404
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11405
|
+
path: ["prompt"],
|
|
11406
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
11407
|
+
});
|
|
11408
|
+
}
|
|
11363
11409
|
});
|
|
11364
11410
|
AgentSoulSchema = exports_external.object({
|
|
11365
11411
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11299,7 +11299,7 @@ var init_zod = __esm(() => {
|
|
|
11299
11299
|
});
|
|
11300
11300
|
|
|
11301
11301
|
// src/config/schema.ts
|
|
11302
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11302
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
11303
11303
|
var init_schema = __esm(() => {
|
|
11304
11304
|
init_zod();
|
|
11305
11305
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -11332,11 +11332,29 @@ var init_schema = __esm(() => {
|
|
|
11332
11332
|
HttpDiffPollSchema,
|
|
11333
11333
|
TelegramReactionsPollSchema
|
|
11334
11334
|
]);
|
|
11335
|
+
TelegramMessageActionSchema = exports_external.object({
|
|
11336
|
+
type: exports_external.literal("telegram-message"),
|
|
11337
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable — fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output — the text is fully determined by config."),
|
|
11338
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
11339
|
+
});
|
|
11340
|
+
WebhookActionSchema = exports_external.object({
|
|
11341
|
+
type: exports_external.literal("webhook"),
|
|
11342
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (§6.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
11343
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
11344
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
11345
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
11346
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (§6.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
11347
|
+
});
|
|
11348
|
+
ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
11349
|
+
TelegramMessageActionSchema,
|
|
11350
|
+
WebhookActionSchema
|
|
11351
|
+
]);
|
|
11335
11352
|
ScheduleEntrySchema = exports_external.object({
|
|
11336
11353
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
11337
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
11338
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
11354
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
11355
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
11339
11356
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
11357
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
11340
11358
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
11341
11359
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' → a minimal-" + "context cheap cron session (Tier 1). 'agent' → the agent's live " + "session with full persona/memory (Tier 2). Unset → inferred from " + "`model` (cheap→fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
11342
11360
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -11353,13 +11371,41 @@ var init_schema = __esm(() => {
|
|
|
11353
11371
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11354
11372
|
});
|
|
11355
11373
|
}
|
|
11356
|
-
if (kind
|
|
11374
|
+
if (kind !== "poll" && entry.poll) {
|
|
11357
11375
|
ctx.addIssue({
|
|
11358
11376
|
code: exports_external.ZodIssueCode.custom,
|
|
11359
11377
|
path: ["poll"],
|
|
11360
11378
|
message: "`poll` is only valid when kind: poll."
|
|
11361
11379
|
});
|
|
11362
11380
|
}
|
|
11381
|
+
if (kind === "action" && !entry.action) {
|
|
11382
|
+
ctx.addIssue({
|
|
11383
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11384
|
+
path: ["action"],
|
|
11385
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
11386
|
+
});
|
|
11387
|
+
}
|
|
11388
|
+
if (kind !== "action" && entry.action) {
|
|
11389
|
+
ctx.addIssue({
|
|
11390
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11391
|
+
path: ["action"],
|
|
11392
|
+
message: "`action` is only valid when kind: action."
|
|
11393
|
+
});
|
|
11394
|
+
}
|
|
11395
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
11396
|
+
ctx.addIssue({
|
|
11397
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11398
|
+
path: ["prompt"],
|
|
11399
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
11400
|
+
});
|
|
11401
|
+
}
|
|
11402
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
11403
|
+
ctx.addIssue({
|
|
11404
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11405
|
+
path: ["prompt"],
|
|
11406
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
11407
|
+
});
|
|
11408
|
+
}
|
|
11363
11409
|
});
|
|
11364
11410
|
AgentSoulSchema = exports_external.object({
|
|
11365
11411
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
package/package.json
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
# container, dedicated to cheap cron fires. It registers to the SAME gateway
|
|
6
6
|
# as a DISTINCT bridge identity `<name>-cron` (start.sh's gateway sidecar
|
|
7
7
|
# routes meta.session=cron fires here and gates all status surfaces off this
|
|
8
|
-
# identity — see telegram-plugin/gateway/cron-session.ts).
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# identity — see telegram-plugin/gateway/cron-session.ts).
|
|
9
|
+
#
|
|
10
|
+
# Tier-1 (#2307) is "a fresh conversation that STILL has the agent's memory +
|
|
11
|
+
# tools, on a cheaper model." It loads the agent's FULL .mcp.json (its own
|
|
12
|
+
# .claude-cron/.mcp.json — same hindsight bank baked from the BASE name, same
|
|
13
|
+
# tools) with ENABLE_TOOL_SEARCH so the schema set DEFERS (low context cost).
|
|
14
|
+
# The saving is dropping the accumulated main-session conversation context +
|
|
15
|
+
# the cheaper model — NOT lobotomising the session. Its CLAUDE_CONFIG_DIR is
|
|
16
|
+
# separate (own transcript) so each fire starts fresh and /clear-friendly.
|
|
12
17
|
#
|
|
13
18
|
# Forked as a supervised sidecar by start.sh ONLY when {{name}} actually has a
|
|
14
19
|
# Tier-1 (context: fresh) cron entry AND SWITCHROOM_CHEAP_CRON is on — so the
|
|
@@ -35,12 +40,13 @@ esac
|
|
|
35
40
|
CRON_NAME="{{name}}-cron"
|
|
36
41
|
CRON_CONFIG_DIR="{{agentDir}}/.claude-cron"
|
|
37
42
|
MAIN_CONFIG_DIR="{{agentDir}}/.claude"
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# .mcp.json if the
|
|
43
|
+
# Cron .mcp.json (scaffold writes .claude-cron/.mcp.json with the agent's FULL
|
|
44
|
+
# filtered MCP set — same hindsight bank, same tools — but the switchroom-
|
|
45
|
+
# telegram entry carries a DISTINCT SWITCHROOM_BRIDGE_ALIVE_PATH so its liveness
|
|
46
|
+
# file can't mask the main bridge). The bridge reads SWITCHROOM_AGENT_NAME from
|
|
47
|
+
# the process env (exported below), NOT its .mcp.json env block, so it registers
|
|
48
|
+
# as the cron identity. Fall back to the main .mcp.json if the cron file is
|
|
49
|
+
# missing (older scaffold) so it still boots.
|
|
44
50
|
CRON_MCP_CONFIG="{{agentDir}}/.claude-cron/.mcp.json"
|
|
45
51
|
[ -f "$CRON_MCP_CONFIG" ] || CRON_MCP_CONFIG="{{agentDir}}/.mcp.json"
|
|
46
52
|
CRON_SOCKET="switchroom-${CRON_NAME}"
|
|
@@ -63,14 +69,33 @@ fi
|
|
|
63
69
|
export SWITCHROOM_AGENT_NAME="$CRON_NAME"
|
|
64
70
|
export CLAUDE_CONFIG_DIR="$CRON_CONFIG_DIR"
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
# Defer the (now full) MCP schema set via tool-search so the cron session keeps
|
|
73
|
+
# its low-context cost — the heavy servers load on demand; switchroom-telegram
|
|
74
|
+
# + hindsight stay alwaysLoad. Normally inherited from start.sh's env; set here
|
|
75
|
+
# as a belt-and-braces default, honouring the operator kill-switch + any value
|
|
76
|
+
# already in the env.
|
|
77
|
+
if [ "${SWITCHROOM_DISABLE_TOOL_SEARCH:-}" != "1" ]; then
|
|
78
|
+
export ENABLE_TOOL_SEARCH="${ENABLE_TOOL_SEARCH:-auto}"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
CRON_APPEND_PROMPT="You are the cheap background cron worker for {{name}}. You run scheduled tasks in a fresh, low-context conversation on a cheaper model — but you SHARE {{name}}'s memory (hindsight) and tools, so use them: recall what you need, look things up, reply with real substance. Do the scheduled task, reply once with the result, and keep it tight."
|
|
67
82
|
|
|
68
83
|
# Launch the cron claude in its OWN tmux session/socket so it never contends
|
|
69
84
|
# with the main session's pane. Interactive (no -p). --strict-mcp-config pins
|
|
70
|
-
# it to the
|
|
71
|
-
# --continue) —
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
# it to the cron .mcp.json (full set, tool-search-deferred). Fresh each boot
|
|
86
|
+
# (no --continue) — low context by construction. cd into the workspace so
|
|
87
|
+
# claude's project key matches the pre-seeded trust state (.claude-cron).
|
|
88
|
+
cd "{{agentDir}}" || exit 1
|
|
89
|
+
# Create the cron tmux session DETACHED (-d), not in attach mode. This is a
|
|
90
|
+
# SUPERVISED BACKGROUND sidecar (start.sh forks it with `&`) with no controlling
|
|
91
|
+
# TTY — the MAIN session owns the container's foreground TTY. The attach flag
|
|
92
|
+
# needs a TTY, so it dies "open terminal failed: not a terminal" and the cron
|
|
93
|
+
# bridge never registers (every Tier-1 fire then falls back to main). `-d` runs
|
|
94
|
+
# claude in a detached pane that registers fine; a human can still
|
|
95
|
+
# `tmux -L "$CRON_SOCKET" attach -t "$CRON_NAME"` later. The `-x/-y` give the
|
|
96
|
+
# pane a size since there's no TTY to derive one from.
|
|
97
|
+
tmux -L "$CRON_SOCKET" \
|
|
98
|
+
new-session -d -s "$CRON_NAME" -x 400 -y 50 \
|
|
74
99
|
claude \
|
|
75
100
|
--dangerously-load-development-channels server:switchroom-telegram \
|
|
76
101
|
--plugin-dir "{{securityPluginDir}}" \
|
|
@@ -79,3 +104,12 @@ exec tmux -L "$CRON_SOCKET" \
|
|
|
79
104
|
--model {{{cronModelQ}}} \
|
|
80
105
|
--append-system-prompt "$CRON_APPEND_PROMPT"{{#if dangerousMode}} \
|
|
81
106
|
--dangerously-skip-permissions{{/if}}
|
|
107
|
+
|
|
108
|
+
# `new-session -d` returns immediately (tmux daemonizes the session), so block
|
|
109
|
+
# here while the session lives — this keeps cron-session.sh as the supervisor's
|
|
110
|
+
# long-running child. When the cron claude exits (crash / kill), the session
|
|
111
|
+
# ends, the loop exits, and the supervisor respawns us cleanly (with backoff).
|
|
112
|
+
while tmux -L "$CRON_SOCKET" has-session -t "$CRON_NAME" 2>/dev/null; do
|
|
113
|
+
sleep 5
|
|
114
|
+
done
|
|
115
|
+
echo "cron-session: ${CRON_NAME} tmux session ended — exiting for supervisor respawn" >&2
|
|
@@ -827,7 +827,13 @@ async function main(): Promise<void> {
|
|
|
827
827
|
onPermission,
|
|
828
828
|
onStatus,
|
|
829
829
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}\n`),
|
|
830
|
-
|
|
830
|
+
// #2307 Tier-1: the cron-session bridge shares the agent's STATE_DIR
|
|
831
|
+
// (access.json / history / gateway.sock) but writes its liveness file to a
|
|
832
|
+
// DISTINCT path (SWITCHROOM_BRIDGE_ALIVE_PATH, set in the cron .mcp.json) so
|
|
833
|
+
// a live <agent>-cron bridge can't mask a dead MAIN bridge in the
|
|
834
|
+
// dashboard/doctor liveness probe (RISK #2). Unset ⟹ the main bridge's
|
|
835
|
+
// canonical STATE_DIR/.bridge-alive, exactly as before.
|
|
836
|
+
livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join(STATE_DIR, ".bridge-alive"),
|
|
831
837
|
})
|
|
832
838
|
if (ipc.isConnected()) {
|
|
833
839
|
process.stderr.write(`telegram bridge: connected to gateway at ${SOCKET_PATH}\n`)
|
|
@@ -25206,7 +25206,7 @@ async function main() {
|
|
|
25206
25206
|
onStatus,
|
|
25207
25207
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}
|
|
25208
25208
|
`),
|
|
25209
|
-
livenessFilePath: join4(STATE_DIR, ".bridge-alive")
|
|
25209
|
+
livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join4(STATE_DIR, ".bridge-alive")
|
|
25210
25210
|
});
|
|
25211
25211
|
if (ipc.isConnected()) {
|
|
25212
25212
|
process.stderr.write(`telegram bridge: connected to gateway at ${SOCKET_PATH}
|
|
@@ -23802,7 +23802,7 @@ var init_dist = __esm(() => {
|
|
|
23802
23802
|
});
|
|
23803
23803
|
|
|
23804
23804
|
// ../src/config/schema.ts
|
|
23805
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
23805
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
23806
23806
|
var init_schema = __esm(() => {
|
|
23807
23807
|
init_zod();
|
|
23808
23808
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -23835,11 +23835,29 @@ var init_schema = __esm(() => {
|
|
|
23835
23835
|
HttpDiffPollSchema,
|
|
23836
23836
|
TelegramReactionsPollSchema
|
|
23837
23837
|
]);
|
|
23838
|
+
TelegramMessageActionSchema = exports_external.object({
|
|
23839
|
+
type: exports_external.literal("telegram-message"),
|
|
23840
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable \u2014 fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output \u2014 the text is fully determined by config."),
|
|
23841
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
23842
|
+
});
|
|
23843
|
+
WebhookActionSchema = exports_external.object({
|
|
23844
|
+
type: exports_external.literal("webhook"),
|
|
23845
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (\u00a76.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
23846
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
23847
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
23848
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
23849
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (\u00a76.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
23850
|
+
});
|
|
23851
|
+
ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
23852
|
+
TelegramMessageActionSchema,
|
|
23853
|
+
WebhookActionSchema
|
|
23854
|
+
]);
|
|
23838
23855
|
ScheduleEntrySchema = exports_external.object({
|
|
23839
23856
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
23840
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
23841
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
23857
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
23858
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
23842
23859
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
23860
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
23843
23861
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
23844
23862
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' \u2192 a minimal-" + "context cheap cron session (Tier 1). 'agent' \u2192 the agent's live " + "session with full persona/memory (Tier 2). Unset \u2192 inferred from " + "`model` (cheap\u2192fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
23845
23863
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default \u2014 broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary \u2014 " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -23856,13 +23874,41 @@ var init_schema = __esm(() => {
|
|
|
23856
23874
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
23857
23875
|
});
|
|
23858
23876
|
}
|
|
23859
|
-
if (kind
|
|
23877
|
+
if (kind !== "poll" && entry.poll) {
|
|
23860
23878
|
ctx.addIssue({
|
|
23861
23879
|
code: exports_external.ZodIssueCode.custom,
|
|
23862
23880
|
path: ["poll"],
|
|
23863
23881
|
message: "`poll` is only valid when kind: poll."
|
|
23864
23882
|
});
|
|
23865
23883
|
}
|
|
23884
|
+
if (kind === "action" && !entry.action) {
|
|
23885
|
+
ctx.addIssue({
|
|
23886
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23887
|
+
path: ["action"],
|
|
23888
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
23889
|
+
});
|
|
23890
|
+
}
|
|
23891
|
+
if (kind !== "action" && entry.action) {
|
|
23892
|
+
ctx.addIssue({
|
|
23893
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23894
|
+
path: ["action"],
|
|
23895
|
+
message: "`action` is only valid when kind: action."
|
|
23896
|
+
});
|
|
23897
|
+
}
|
|
23898
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
23899
|
+
ctx.addIssue({
|
|
23900
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23901
|
+
path: ["prompt"],
|
|
23902
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
23903
|
+
});
|
|
23904
|
+
}
|
|
23905
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
23906
|
+
ctx.addIssue({
|
|
23907
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23908
|
+
path: ["prompt"],
|
|
23909
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
23910
|
+
});
|
|
23911
|
+
}
|
|
23866
23912
|
});
|
|
23867
23913
|
AgentSoulSchema = exports_external.object({
|
|
23868
23914
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -46752,6 +46798,17 @@ function createInjectIpcClient(options) {
|
|
|
46752
46798
|
return false;
|
|
46753
46799
|
try {
|
|
46754
46800
|
return socket.write(JSON.stringify(msg) + `
|
|
46801
|
+
`);
|
|
46802
|
+
} catch (err) {
|
|
46803
|
+
log(`scheduler ipc: write failed: ${err.message}`);
|
|
46804
|
+
return false;
|
|
46805
|
+
}
|
|
46806
|
+
},
|
|
46807
|
+
sendOutbound(msg) {
|
|
46808
|
+
if (!socket || !connected)
|
|
46809
|
+
return false;
|
|
46810
|
+
try {
|
|
46811
|
+
return socket.write(JSON.stringify(msg) + `
|
|
46755
46812
|
`);
|
|
46756
46813
|
} catch (err) {
|
|
46757
46814
|
log(`scheduler ipc: write failed: ${err.message}`);
|
|
@@ -47327,6 +47384,19 @@ function validateClientMessage(msg) {
|
|
|
47327
47384
|
const inb = m.inbound;
|
|
47328
47385
|
return inb.type === "inbound" && typeof inb.chatId === "string" && inb.chatId.length > 0 && typeof inb.text === "string" && typeof inb.messageId === "number" && typeof inb.user === "string" && typeof inb.userId === "number" && typeof inb.ts === "number" && typeof inb.meta === "object" && inb.meta !== null;
|
|
47329
47386
|
}
|
|
47387
|
+
case "send_outbound": {
|
|
47388
|
+
if (typeof m.agentName !== "string" || !AGENT_NAME_RE3.test(m.agentName))
|
|
47389
|
+
return false;
|
|
47390
|
+
if (typeof m.chatId !== "string" || m.chatId.length === 0)
|
|
47391
|
+
return false;
|
|
47392
|
+
if (typeof m.text !== "string" || m.text.length === 0 || m.text.length > 4096)
|
|
47393
|
+
return false;
|
|
47394
|
+
if (m.threadId !== undefined && (typeof m.threadId !== "number" || !Number.isInteger(m.threadId)))
|
|
47395
|
+
return false;
|
|
47396
|
+
if (m.parseMode !== undefined && m.parseMode !== "html" && m.parseMode !== "text")
|
|
47397
|
+
return false;
|
|
47398
|
+
return true;
|
|
47399
|
+
}
|
|
47330
47400
|
case "quota_wall_detected": {
|
|
47331
47401
|
if (typeof m.agentName !== "string" || !AGENT_NAME_RE3.test(m.agentName))
|
|
47332
47402
|
return false;
|
|
@@ -47395,6 +47465,7 @@ function createIpcServer(options) {
|
|
|
47395
47465
|
onOperatorEvent,
|
|
47396
47466
|
onPtyPartial,
|
|
47397
47467
|
onInjectInbound,
|
|
47468
|
+
onSendOutbound,
|
|
47398
47469
|
onQuotaWallDetected,
|
|
47399
47470
|
onRequestDriveApproval,
|
|
47400
47471
|
onRequestMs365Approval,
|
|
@@ -47480,6 +47551,10 @@ function createIpcServer(options) {
|
|
|
47480
47551
|
if (onInjectInbound)
|
|
47481
47552
|
onInjectInbound(client3, msg);
|
|
47482
47553
|
break;
|
|
47554
|
+
case "send_outbound":
|
|
47555
|
+
if (onSendOutbound)
|
|
47556
|
+
onSendOutbound(client3, msg);
|
|
47557
|
+
break;
|
|
47483
47558
|
case "quota_wall_detected":
|
|
47484
47559
|
if (onQuotaWallDetected)
|
|
47485
47560
|
onQuotaWallDetected(client3, msg);
|
|
@@ -54091,11 +54166,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54091
54166
|
}
|
|
54092
54167
|
|
|
54093
54168
|
// ../src/build-info.ts
|
|
54094
|
-
var VERSION = "0.15.
|
|
54095
|
-
var COMMIT_SHA = "
|
|
54096
|
-
var COMMIT_DATE = "2026-06-
|
|
54097
|
-
var LATEST_PR =
|
|
54098
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
54169
|
+
var VERSION = "0.15.15";
|
|
54170
|
+
var COMMIT_SHA = "815adf85";
|
|
54171
|
+
var COMMIT_DATE = "2026-06-13T12:14:07Z";
|
|
54172
|
+
var LATEST_PR = 2328;
|
|
54173
|
+
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54099
54174
|
|
|
54100
54175
|
// gateway/boot-version.ts
|
|
54101
54176
|
function formatRelativeAgo(iso) {
|
|
@@ -57690,6 +57765,7 @@ var ipcServer = createIpcServer({
|
|
|
57690
57765
|
if (fellBackToMain) {
|
|
57691
57766
|
process.stderr.write(`telegram gateway: cron fire fell back to main session (no cron bridge) agent=${msg.agentName} prompt_key=${promptKey}
|
|
57692
57767
|
`);
|
|
57768
|
+
emitRuntimeMetric({ kind: "cron_fell_back_to_main", agent: msg.agentName, prompt_key: promptKey });
|
|
57693
57769
|
}
|
|
57694
57770
|
if (delivered && target === msg.agentName)
|
|
57695
57771
|
markClaudeBusyForInbound(msg.inbound);
|
|
@@ -57699,6 +57775,29 @@ var ipcServer = createIpcServer({
|
|
|
57699
57775
|
pendingInboundBuffer.push(target, msg.inbound);
|
|
57700
57776
|
}
|
|
57701
57777
|
},
|
|
57778
|
+
onSendOutbound(_client, msg) {
|
|
57779
|
+
const self2 = process.env.SWITCHROOM_AGENT_NAME;
|
|
57780
|
+
if (self2 && msg.agentName !== self2) {
|
|
57781
|
+
process.stderr.write(`telegram gateway: send_outbound rejected \u2014 agent mismatch (${msg.agentName} != ${self2})
|
|
57782
|
+
`);
|
|
57783
|
+
return;
|
|
57784
|
+
}
|
|
57785
|
+
try {
|
|
57786
|
+
assertAllowedChat(msg.chatId);
|
|
57787
|
+
} catch (err) {
|
|
57788
|
+
process.stderr.write(`telegram gateway: send_outbound rejected \u2014 ${err.message}
|
|
57789
|
+
`);
|
|
57790
|
+
return;
|
|
57791
|
+
}
|
|
57792
|
+
const threadId = msg.threadId;
|
|
57793
|
+
const parseMode = msg.parseMode === "text" ? undefined : "HTML";
|
|
57794
|
+
swallowingApiCall(() => bot.api.sendMessage(msg.chatId, msg.text, {
|
|
57795
|
+
...parseMode ? { parse_mode: parseMode } : {},
|
|
57796
|
+
...threadId != null && threadId !== 1 ? { message_thread_id: threadId } : {}
|
|
57797
|
+
}), { chat_id: msg.chatId, verb: "cron-action-send", ...threadId != null ? { threadId } : {} });
|
|
57798
|
+
process.stderr.write(`telegram gateway: send_outbound agent=${msg.agentName} chat=${msg.chatId} thread=${threadId ?? "-"} len=${msg.text.length}
|
|
57799
|
+
`);
|
|
57800
|
+
},
|
|
57702
57801
|
onQuotaWallDetected(_client, msg) {
|
|
57703
57802
|
const untilMs = resolveExhaustUntil(msg.resetAt);
|
|
57704
57803
|
process.stderr.write(`telegram gateway: quota_wall_detected agent=${msg.agentName} until=${new Date(untilMs).toISOString()}` + (msg.resetAt == null ? " (reset unparsed \u2192 +7d default)" : "") + ` \u2014 triggering fleet auto-fallback
|
|
@@ -24221,7 +24221,7 @@ async function main() {
|
|
|
24221
24221
|
onStatus,
|
|
24222
24222
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}
|
|
24223
24223
|
`),
|
|
24224
|
-
livenessFilePath: join5(STATE_DIR, ".bridge-alive")
|
|
24224
|
+
livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join5(STATE_DIR, ".bridge-alive")
|
|
24225
24225
|
});
|
|
24226
24226
|
if (ipc.isConnected()) {
|
|
24227
24227
|
process.stderr.write(`telegram bridge: connected to gateway at ${SOCKET_PATH}
|