switchroom 0.15.10 → 0.15.12
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 +7 -80
- package/dist/auth-broker/index.js +5 -0
- package/dist/cli/notion-write-pretool.mjs +5 -0
- package/dist/cli/switchroom.js +292 -261
- package/dist/host-control/main.js +5 -0
- package/dist/vault/approvals/kernel-server.js +5 -0
- package/dist/vault/broker/server.js +5 -0
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +10 -6
- package/telegram-plugin/bridge/bridge.ts +24 -0
- package/telegram-plugin/dist/bridge/bridge.js +23 -0
- package/telegram-plugin/dist/gateway/gateway.js +266 -23
- package/telegram-plugin/dist/server.js +23 -0
- package/telegram-plugin/gateway/gateway.ts +100 -24
- package/telegram-plugin/gateway/linear-activity.ts +160 -0
- package/telegram-plugin/gateway/model-command.ts +13 -5
- package/telegram-plugin/gateway/obligation-ledger.ts +56 -15
- package/telegram-plugin/history.ts +57 -0
- package/telegram-plugin/tests/gateway-request-secret.test.ts +1 -1
- package/telegram-plugin/tests/history.test.ts +83 -0
- package/telegram-plugin/tests/linear-agent-activity.test.ts +124 -0
- package/telegram-plugin/tests/model-command.test.ts +40 -0
- package/telegram-plugin/tests/obligation-ledger.test.ts +213 -5
- package/telegram-plugin/tests/obligation-store.test.ts +17 -0
|
@@ -13882,6 +13882,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
13882
13882
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
13883
13883
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
13884
13884
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
13885
|
+
linear_agent: exports_external.object({
|
|
13886
|
+
enabled: exports_external.boolean(),
|
|
13887
|
+
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
13888
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
13889
|
+
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
13885
13890
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
13886
13891
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
13887
13892
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
@@ -11480,6 +11480,11 @@ var init_schema = __esm(() => {
|
|
|
11480
11480
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
11481
11481
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11482
11482
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11483
|
+
linear_agent: exports_external.object({
|
|
11484
|
+
enabled: exports_external.boolean(),
|
|
11485
|
+
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
11486
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
11487
|
+
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
11483
11488
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11484
11489
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11485
11490
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
@@ -11480,6 +11480,11 @@ var init_schema = __esm(() => {
|
|
|
11480
11480
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
11481
11481
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11482
11482
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11483
|
+
linear_agent: exports_external.object({
|
|
11484
|
+
enabled: exports_external.boolean(),
|
|
11485
|
+
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
11486
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
11487
|
+
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
11483
11488
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11484
11489
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11485
11490
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
package/package.json
CHANGED
|
@@ -17,15 +17,19 @@
|
|
|
17
17
|
set -u
|
|
18
18
|
|
|
19
19
|
# Runtime kill-switch. The fork is baked into start.sh whenever {{name}} has a
|
|
20
|
-
#
|
|
21
|
-
# runs when
|
|
22
|
-
#
|
|
20
|
+
# cron entry the value-gate routes to a cheap session, but the session only
|
|
21
|
+
# actually runs when cheap-cron is enabled at runtime. Cheap-cron is ON by
|
|
22
|
+
# DEFAULT (matches isCheapCronEnabled in src/scheduler/cron-routing.ts — only
|
|
23
|
+
# SWITCHROOM_CHEAP_CRON=0/false/off disables it); the old "off unless =1" gate
|
|
24
|
+
# here meant the cron session quarantined itself even with cheap-by-default on,
|
|
25
|
+
# so every Tier-1 fire fell back to the main session and saved nothing. Exit 78
|
|
26
|
+
# (EX_CONFIG) on the explicit kill-switch so the supervisor cleanly idles.
|
|
23
27
|
case "${SWITCHROOM_CHEAP_CRON:-}" in
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
echo "cron-session: SWITCHROOM_CHEAP_CRON off — not starting (exit 78, no respawn)" >&2
|
|
28
|
+
0 | false | off | OFF | False | FALSE)
|
|
29
|
+
echo "cron-session: SWITCHROOM_CHEAP_CRON disabled (=0) — not starting (exit 78, no respawn)" >&2
|
|
27
30
|
exit 78
|
|
28
31
|
;;
|
|
32
|
+
*) : ;;
|
|
29
33
|
esac
|
|
30
34
|
|
|
31
35
|
CRON_NAME="{{name}}-cron"
|
|
@@ -455,6 +455,30 @@ const TOOL_SCHEMAS = [
|
|
|
455
455
|
required: ['chat_id', 'key'],
|
|
456
456
|
},
|
|
457
457
|
},
|
|
458
|
+
{
|
|
459
|
+
name: 'linear_agent_activity',
|
|
460
|
+
description:
|
|
461
|
+
'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) — pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge → work → result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: {
|
|
465
|
+
agent_session_id: {
|
|
466
|
+
type: 'string',
|
|
467
|
+
description: 'The Linear AgentSession id — copy it verbatim from the woken turn\'s meta.agent_session_id.',
|
|
468
|
+
},
|
|
469
|
+
type: {
|
|
470
|
+
type: 'string',
|
|
471
|
+
enum: ['thought', 'message', 'complete', 'error'],
|
|
472
|
+
description: 'Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure.',
|
|
473
|
+
},
|
|
474
|
+
body: {
|
|
475
|
+
type: 'string',
|
|
476
|
+
description: 'Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary).',
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
required: ['agent_session_id', 'type'],
|
|
480
|
+
},
|
|
481
|
+
},
|
|
458
482
|
]
|
|
459
483
|
|
|
460
484
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
|
|
@@ -24934,6 +24934,29 @@ var TOOL_SCHEMAS = [
|
|
|
24934
24934
|
},
|
|
24935
24935
|
required: ["chat_id", "key"]
|
|
24936
24936
|
}
|
|
24937
|
+
},
|
|
24938
|
+
{
|
|
24939
|
+
name: "linear_agent_activity",
|
|
24940
|
+
description: 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) \u2014 pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge \u2192 work \u2192 result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
|
|
24941
|
+
inputSchema: {
|
|
24942
|
+
type: "object",
|
|
24943
|
+
properties: {
|
|
24944
|
+
agent_session_id: {
|
|
24945
|
+
type: "string",
|
|
24946
|
+
description: "The Linear AgentSession id \u2014 copy it verbatim from the woken turn's meta.agent_session_id."
|
|
24947
|
+
},
|
|
24948
|
+
type: {
|
|
24949
|
+
type: "string",
|
|
24950
|
+
enum: ["thought", "message", "complete", "error"],
|
|
24951
|
+
description: "Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure."
|
|
24952
|
+
},
|
|
24953
|
+
body: {
|
|
24954
|
+
type: "string",
|
|
24955
|
+
description: "Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary)."
|
|
24956
|
+
}
|
|
24957
|
+
},
|
|
24958
|
+
required: ["agent_session_id", "type"]
|
|
24959
|
+
}
|
|
24937
24960
|
}
|
|
24938
24961
|
];
|
|
24939
24962
|
mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools: TOOL_SCHEMAS }));
|
|
@@ -23993,6 +23993,11 @@ var init_schema = __esm(() => {
|
|
|
23993
23993
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
23994
23994
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
23995
23995
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
23996
|
+
linear_agent: exports_external.object({
|
|
23997
|
+
enabled: exports_external.boolean(),
|
|
23998
|
+
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
23999
|
+
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
|
|
24000
|
+
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
23996
24001
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
23997
24002
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
23998
24003
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
|
|
@@ -28268,6 +28273,7 @@ __export(exports_history, {
|
|
|
28268
28273
|
pruneMessagesOlderThanDays: () => pruneMessagesOlderThanDays,
|
|
28269
28274
|
lookupMessageRoleAndText: () => lookupMessageRoleAndText,
|
|
28270
28275
|
initHistory: () => initHistory,
|
|
28276
|
+
hasOutboundDeliveredSince: () => hasOutboundDeliveredSince,
|
|
28271
28277
|
getRecentOutboundCount: () => getRecentOutboundCount,
|
|
28272
28278
|
getLatestInboundMessageId: () => getLatestInboundMessageId,
|
|
28273
28279
|
deleteFromHistory: () => deleteFromHistory,
|
|
@@ -28462,6 +28468,26 @@ function getRecentOutboundCount(chatId, withinSeconds) {
|
|
|
28462
28468
|
const row = requireDb().prepare("SELECT COUNT(*) as cnt FROM messages WHERE chat_id = ? AND role = ? AND ts >= ?").get(chatId, "assistant", cutoff);
|
|
28463
28469
|
return row?.cnt ?? 0;
|
|
28464
28470
|
}
|
|
28471
|
+
function hasOutboundDeliveredSince(chatId, sinceMs, threadId) {
|
|
28472
|
+
try {
|
|
28473
|
+
const cutoffSec = Math.floor(sinceMs / 1000);
|
|
28474
|
+
const params = [chatId, cutoffSec];
|
|
28475
|
+
let sql = "SELECT 1 FROM messages WHERE chat_id = ? AND role = 'assistant' AND ts >= ? AND LENGTH(text) >= 200";
|
|
28476
|
+
if (threadId !== undefined) {
|
|
28477
|
+
if (threadId === null) {
|
|
28478
|
+
sql += " AND thread_id IS NULL";
|
|
28479
|
+
} else {
|
|
28480
|
+
sql += " AND thread_id = ?";
|
|
28481
|
+
params.push(threadId);
|
|
28482
|
+
}
|
|
28483
|
+
}
|
|
28484
|
+
sql += " LIMIT 1";
|
|
28485
|
+
const row = requireDb().prepare(sql).get(...params);
|
|
28486
|
+
return row != null;
|
|
28487
|
+
} catch {
|
|
28488
|
+
return false;
|
|
28489
|
+
}
|
|
28490
|
+
}
|
|
28465
28491
|
function query(opts) {
|
|
28466
28492
|
const limit = Math.min(MAX_LIMIT, Math.max(1, opts.limit ?? DEFAULT_LIMIT));
|
|
28467
28493
|
const params = [opts.chat_id];
|
|
@@ -44899,7 +44925,7 @@ function labelTag(label) {
|
|
|
44899
44925
|
}
|
|
44900
44926
|
|
|
44901
44927
|
// gateway/model-command.ts
|
|
44902
|
-
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "default"];
|
|
44928
|
+
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
|
|
44903
44929
|
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,99}$/;
|
|
44904
44930
|
function isValidModelArg(arg) {
|
|
44905
44931
|
return MODEL_ARG_RE.test(arg);
|
|
@@ -47005,6 +47031,53 @@ function injectWebhookInbound(agent, prompt, ctx, deps = {}) {
|
|
|
47005
47031
|
`)).catch((err) => log(`webhook-dispatch: agent='${agent}' inject failed: ${String(err)}
|
|
47006
47032
|
`));
|
|
47007
47033
|
}
|
|
47034
|
+
function parseLinearAgentSession(payload) {
|
|
47035
|
+
const type = String(payload.type ?? "").toLowerCase();
|
|
47036
|
+
if (type !== "agentsessionevent")
|
|
47037
|
+
return null;
|
|
47038
|
+
const action = String(payload.action ?? "").toLowerCase();
|
|
47039
|
+
const session = payload.agentSession ?? payload.agent_session ?? {};
|
|
47040
|
+
const sessionId = String(session.id ?? payload.agentSessionId ?? payload.agent_session_id ?? "");
|
|
47041
|
+
if (!sessionId)
|
|
47042
|
+
return null;
|
|
47043
|
+
const explicitTrigger = String(session.trigger ?? payload.trigger ?? "").toLowerCase();
|
|
47044
|
+
const comment = session.comment;
|
|
47045
|
+
const trigger = explicitTrigger === "mention" || explicitTrigger === "delegation" ? explicitTrigger : comment ? "mention" : "delegation";
|
|
47046
|
+
const promptContext = typeof payload.promptContext === "string" ? payload.promptContext : typeof session.promptContext === "string" ? session.promptContext : undefined;
|
|
47047
|
+
const issue = session.issue ?? {};
|
|
47048
|
+
const issueId = String(issue.identifier ?? "");
|
|
47049
|
+
const issueTitle = String(issue.title ?? "");
|
|
47050
|
+
const commentBody = typeof comment?.body === "string" ? comment.body : "";
|
|
47051
|
+
const summaryParts = [];
|
|
47052
|
+
summaryParts.push(trigger === "mention" ? `You were @mentioned in Linear` : `An issue was delegated to you in Linear`);
|
|
47053
|
+
if (issueId || issueTitle) {
|
|
47054
|
+
summaryParts.push(`Issue ${issueId}${issueTitle ? `: ${issueTitle}` : ""}`.trim());
|
|
47055
|
+
}
|
|
47056
|
+
if (commentBody)
|
|
47057
|
+
summaryParts.push(commentBody);
|
|
47058
|
+
const summary = summaryParts.join(`
|
|
47059
|
+
`);
|
|
47060
|
+
return { sessionId, trigger, action, promptContext, summary };
|
|
47061
|
+
}
|
|
47062
|
+
function buildLinearInbound(session, ctx, now) {
|
|
47063
|
+
const content = session.promptContext && session.promptContext.trim().length > 0 ? session.promptContext : session.summary;
|
|
47064
|
+
return {
|
|
47065
|
+
type: "inbound",
|
|
47066
|
+
chatId: ctx.chatId,
|
|
47067
|
+
...ctx.threadId !== undefined ? { threadId: ctx.threadId } : {},
|
|
47068
|
+
messageId: now,
|
|
47069
|
+
user: "linear",
|
|
47070
|
+
userId: 0,
|
|
47071
|
+
ts: now,
|
|
47072
|
+
text: content,
|
|
47073
|
+
meta: {
|
|
47074
|
+
source: "linear",
|
|
47075
|
+
agent_session_id: session.sessionId,
|
|
47076
|
+
linear_trigger: session.trigger,
|
|
47077
|
+
linear_event: `agent_session.${session.action || "event"}`
|
|
47078
|
+
}
|
|
47079
|
+
};
|
|
47080
|
+
}
|
|
47008
47081
|
function evaluateDispatch(args, deps = {}) {
|
|
47009
47082
|
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
47010
47083
|
const now = (deps.now ?? Date.now)();
|
|
@@ -47116,6 +47189,38 @@ function recordWebhookEvent(rec, deps = {}) {
|
|
|
47116
47189
|
}
|
|
47117
47190
|
log(`webhook-gateway: agent='${agent}' source='${rec.source}' event='${rec.event_type}' recorded ts=${now}
|
|
47118
47191
|
`);
|
|
47192
|
+
if (rec.source === "linear") {
|
|
47193
|
+
try {
|
|
47194
|
+
const session = parseLinearAgentSession(rec.payload);
|
|
47195
|
+
if (session) {
|
|
47196
|
+
const config = (deps.loadConfig ?? loadConfig)();
|
|
47197
|
+
const rawAgent = config.agents?.[agent];
|
|
47198
|
+
const linearAgent = rawAgent ? resolveAgentConfig(config.defaults, config.profiles, rawAgent).channels?.telegram?.linear_agent : undefined;
|
|
47199
|
+
if (!linearAgent?.enabled) {
|
|
47200
|
+
log(`linear-agent: agent='${agent}' session='${session.sessionId}' ignored \u2014 linear_agent not enabled
|
|
47201
|
+
`);
|
|
47202
|
+
} else {
|
|
47203
|
+
const target = resolveChannelTarget(config, agent);
|
|
47204
|
+
if (!target) {
|
|
47205
|
+
log(`linear-agent: agent='${agent}' session='${session.sessionId}' skipped \u2014 no chat target (forum_chat_id / chat_id unset)
|
|
47206
|
+
`);
|
|
47207
|
+
} else {
|
|
47208
|
+
const inbound = buildLinearInbound(session, {
|
|
47209
|
+
chatId: target.chatId,
|
|
47210
|
+
...target.threadId !== undefined ? { threadId: target.threadId } : {}
|
|
47211
|
+
}, now);
|
|
47212
|
+
const ok = deps.inject ? deps.inject(agent, inbound) : false;
|
|
47213
|
+
log(`linear-agent: agent='${agent}' session='${session.sessionId}' trigger='${session.trigger}' ` + `${ok ? "injected" : "NOT injected (no inject sink)"}
|
|
47214
|
+
`);
|
|
47215
|
+
return { status: "ok", ts: now, dispatched: ok ? 1 : 0 };
|
|
47216
|
+
}
|
|
47217
|
+
}
|
|
47218
|
+
}
|
|
47219
|
+
} catch (err) {
|
|
47220
|
+
log(`linear-agent: agent='${agent}' session inject error (event recorded): ${err.message}
|
|
47221
|
+
`);
|
|
47222
|
+
}
|
|
47223
|
+
}
|
|
47119
47224
|
let dispatched = 0;
|
|
47120
47225
|
try {
|
|
47121
47226
|
const config = (deps.loadConfig ?? loadConfig)();
|
|
@@ -48305,21 +48410,23 @@ class ObligationLedger {
|
|
|
48305
48410
|
return best;
|
|
48306
48411
|
}
|
|
48307
48412
|
decideAtIdle(opts) {
|
|
48308
|
-
const useEligible = opts != null && (opts.graceMs > 0 || opts.backgroundWorkActive === true);
|
|
48309
|
-
const o = useEligible ? this.oldestEligible(opts.now, opts.graceMs, opts.backgroundWorkActive === true, opts.backgroundGraceMs ?? 0) : this.oldest();
|
|
48413
|
+
const useEligible = opts != null && (opts.graceMs > 0 || opts.backgroundWorkActive === true || (opts.representGraceMs ?? 0) > 0);
|
|
48414
|
+
const o = useEligible ? this.oldestEligible(opts.now, opts.graceMs, opts.backgroundWorkActive === true, opts.backgroundGraceMs ?? 0, opts.representGraceMs ?? 0) : this.oldest();
|
|
48310
48415
|
if (o === undefined)
|
|
48311
48416
|
return { action: "none" };
|
|
48312
48417
|
if (o.representCount >= this.maxRepresents)
|
|
48313
48418
|
return { action: "escalate", obligation: o };
|
|
48314
48419
|
return { action: "represent", obligation: o };
|
|
48315
48420
|
}
|
|
48316
|
-
oldestEligible(now, graceMs, backgroundWorkActive, backgroundGraceMs) {
|
|
48421
|
+
oldestEligible(now, graceMs, backgroundWorkActive, backgroundGraceMs, representGraceMs) {
|
|
48317
48422
|
let best;
|
|
48318
48423
|
for (const o of this.open.values()) {
|
|
48319
48424
|
if (o.lastTurnEndedAt != null && now - o.lastTurnEndedAt < graceMs)
|
|
48320
48425
|
continue;
|
|
48321
48426
|
if (backgroundWorkActive && backgroundGraceMs > 0 && now - o.openedAt < backgroundGraceMs)
|
|
48322
48427
|
continue;
|
|
48428
|
+
if (representGraceMs > 0 && o.lastRepresentedAt != null && now - o.lastRepresentedAt < representGraceMs)
|
|
48429
|
+
continue;
|
|
48323
48430
|
if (best === undefined || o.openedAt < best.openedAt)
|
|
48324
48431
|
best = o;
|
|
48325
48432
|
}
|
|
@@ -48332,18 +48439,21 @@ class ObligationLedger {
|
|
|
48332
48439
|
o.lastTurnEndedAt = ts;
|
|
48333
48440
|
this.persist();
|
|
48334
48441
|
}
|
|
48335
|
-
resolveCloseTarget(echoedTurnId, liveTurnId) {
|
|
48442
|
+
resolveCloseTarget(echoedTurnId, liveTurnId, routedOriginId) {
|
|
48336
48443
|
if (echoedTurnId != null)
|
|
48337
48444
|
return echoedTurnId;
|
|
48338
|
-
if (
|
|
48445
|
+
if (routedOriginId != null)
|
|
48446
|
+
return routedOriginId;
|
|
48447
|
+
if (liveTurnId != null && this.open.has(liveTurnId))
|
|
48339
48448
|
return liveTurnId;
|
|
48340
48449
|
return null;
|
|
48341
48450
|
}
|
|
48342
|
-
markRepresented(originTurnId) {
|
|
48451
|
+
markRepresented(originTurnId, now = Date.now()) {
|
|
48343
48452
|
const o = this.open.get(originTurnId);
|
|
48344
48453
|
if (o === undefined)
|
|
48345
48454
|
return 0;
|
|
48346
48455
|
o.representCount += 1;
|
|
48456
|
+
o.lastRepresentedAt = now;
|
|
48347
48457
|
this.persist();
|
|
48348
48458
|
return o.representCount;
|
|
48349
48459
|
}
|
|
@@ -53767,10 +53877,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53767
53877
|
}
|
|
53768
53878
|
|
|
53769
53879
|
// ../src/build-info.ts
|
|
53770
|
-
var VERSION = "0.15.
|
|
53771
|
-
var COMMIT_SHA = "
|
|
53772
|
-
var COMMIT_DATE = "2026-06-
|
|
53773
|
-
var LATEST_PR =
|
|
53880
|
+
var VERSION = "0.15.12";
|
|
53881
|
+
var COMMIT_SHA = "18b7b6e6";
|
|
53882
|
+
var COMMIT_DATE = "2026-06-13T04:55:14Z";
|
|
53883
|
+
var LATEST_PR = 2311;
|
|
53774
53884
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53775
53885
|
|
|
53776
53886
|
// gateway/boot-version.ts
|
|
@@ -54061,6 +54171,110 @@ async function revokeGrantViaBroker(id, opts) {
|
|
|
54061
54171
|
return { kind: "error", msg: "unexpected broker response" };
|
|
54062
54172
|
}
|
|
54063
54173
|
|
|
54174
|
+
// gateway/linear-activity.ts
|
|
54175
|
+
init_client2();
|
|
54176
|
+
var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
|
|
54177
|
+
async function defaultResolveLinearToken(agent) {
|
|
54178
|
+
const key = `linear/${agent}/token`;
|
|
54179
|
+
const token = readVaultTokenFile(agent) ?? undefined;
|
|
54180
|
+
const result = await getViaBrokerStructured(key, token ? { token } : {});
|
|
54181
|
+
if (result.kind === "ok" && result.entry.kind === "string") {
|
|
54182
|
+
return { ok: true, token: result.entry.value };
|
|
54183
|
+
}
|
|
54184
|
+
if (result.kind === "unreachable")
|
|
54185
|
+
return { ok: false, reason: "unreachable" };
|
|
54186
|
+
if (result.kind === "not_found")
|
|
54187
|
+
return { ok: false, reason: "not_found" };
|
|
54188
|
+
if (result.kind === "denied")
|
|
54189
|
+
return { ok: false, reason: "denied" };
|
|
54190
|
+
return { ok: false, reason: "unknown" };
|
|
54191
|
+
}
|
|
54192
|
+
async function emitLinearAgentActivity(args, deps = {}) {
|
|
54193
|
+
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
54194
|
+
const sessionId = args.agent_session_id;
|
|
54195
|
+
if (!sessionId)
|
|
54196
|
+
throw new Error("linear_agent_activity: agent_session_id is required");
|
|
54197
|
+
const type = args.type;
|
|
54198
|
+
if (!type || !["thought", "message", "complete", "error"].includes(type)) {
|
|
54199
|
+
throw new Error("linear_agent_activity: type must be one of thought|message|complete|error");
|
|
54200
|
+
}
|
|
54201
|
+
const body = args.body;
|
|
54202
|
+
if (type !== "complete" && (body == null || body === "")) {
|
|
54203
|
+
throw new Error(`linear_agent_activity: body is required for type='${type}'`);
|
|
54204
|
+
}
|
|
54205
|
+
const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? "-";
|
|
54206
|
+
const resolveToken = deps.resolveToken ?? defaultResolveLinearToken;
|
|
54207
|
+
const tokenResult = await resolveToken(agent);
|
|
54208
|
+
if (!tokenResult.ok) {
|
|
54209
|
+
if (tokenResult.reason === "denied" || tokenResult.reason === "not_found") {
|
|
54210
|
+
return {
|
|
54211
|
+
content: [
|
|
54212
|
+
{
|
|
54213
|
+
type: "text",
|
|
54214
|
+
text: `linear_agent_activity failed: no Linear token (vault ${tokenResult.reason}). ` + `Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.`
|
|
54215
|
+
}
|
|
54216
|
+
]
|
|
54217
|
+
};
|
|
54218
|
+
}
|
|
54219
|
+
return {
|
|
54220
|
+
content: [
|
|
54221
|
+
{
|
|
54222
|
+
type: "text",
|
|
54223
|
+
text: `linear_agent_activity failed: vault broker ${tokenResult.reason} resolving 'linear/${agent}/token'.`
|
|
54224
|
+
}
|
|
54225
|
+
]
|
|
54226
|
+
};
|
|
54227
|
+
}
|
|
54228
|
+
const content = { type };
|
|
54229
|
+
if (body != null && body !== "")
|
|
54230
|
+
content.body = body;
|
|
54231
|
+
const mutation = "mutation AgentActivityCreate($input: AgentActivityCreateInput!) { " + "agentActivityCreate(input: $input) { success agentActivity { id } } }";
|
|
54232
|
+
const variables = { input: { agentSessionId: sessionId, content } };
|
|
54233
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
54234
|
+
let resp;
|
|
54235
|
+
try {
|
|
54236
|
+
resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
|
|
54237
|
+
method: "POST",
|
|
54238
|
+
headers: {
|
|
54239
|
+
"Content-Type": "application/json",
|
|
54240
|
+
Authorization: tokenResult.token
|
|
54241
|
+
},
|
|
54242
|
+
body: JSON.stringify({ query: mutation, variables })
|
|
54243
|
+
});
|
|
54244
|
+
} catch (err) {
|
|
54245
|
+
return {
|
|
54246
|
+
content: [{ type: "text", text: `linear_agent_activity failed: request error: ${err.message}` }]
|
|
54247
|
+
};
|
|
54248
|
+
}
|
|
54249
|
+
if (!resp.ok) {
|
|
54250
|
+
const txt = await resp.text().catch(() => "");
|
|
54251
|
+
return {
|
|
54252
|
+
content: [
|
|
54253
|
+
{ type: "text", text: `linear_agent_activity failed: Linear API ${resp.status}${txt ? ` \u2014 ${txt.slice(0, 200)}` : ""}` }
|
|
54254
|
+
]
|
|
54255
|
+
};
|
|
54256
|
+
}
|
|
54257
|
+
let json;
|
|
54258
|
+
try {
|
|
54259
|
+
json = await resp.json();
|
|
54260
|
+
} catch {
|
|
54261
|
+
return { content: [{ type: "text", text: "linear_agent_activity failed: malformed Linear API response" }] };
|
|
54262
|
+
}
|
|
54263
|
+
if (json.errors && json.errors.length > 0) {
|
|
54264
|
+
return {
|
|
54265
|
+
content: [
|
|
54266
|
+
{ type: "text", text: `linear_agent_activity failed: ${json.errors.map((e) => e.message ?? "error").join("; ").slice(0, 300)}` }
|
|
54267
|
+
]
|
|
54268
|
+
};
|
|
54269
|
+
}
|
|
54270
|
+
if (json.data?.agentActivityCreate?.success === false) {
|
|
54271
|
+
return { content: [{ type: "text", text: "linear_agent_activity failed: Linear reported success=false" }] };
|
|
54272
|
+
}
|
|
54273
|
+
log(`telegram gateway: linear_agent_activity: emitted type=${type} session=${sessionId} agent=${agent}
|
|
54274
|
+
`);
|
|
54275
|
+
return { content: [{ type: "text", text: `Linear ${type} emitted on session ${sessionId}` }] };
|
|
54276
|
+
}
|
|
54277
|
+
|
|
54064
54278
|
// vault-approval-posture.ts
|
|
54065
54279
|
function resolveVaultApprovalPosture(broker) {
|
|
54066
54280
|
if (broker?.approvalAuth === "telegram-id") {
|
|
@@ -55003,6 +55217,13 @@ var OBLIGATION_BACKGROUND_WORK_GRACE_MS = (() => {
|
|
|
55003
55217
|
const n = Number(raw);
|
|
55004
55218
|
return Number.isFinite(n) && n >= 0 ? n : 1200000;
|
|
55005
55219
|
})();
|
|
55220
|
+
var OBLIGATION_REPRESENT_GRACE_MS = (() => {
|
|
55221
|
+
const raw = process.env.SWITCHROOM_OBLIGATION_REPRESENT_GRACE_MS;
|
|
55222
|
+
if (raw == null || raw === "")
|
|
55223
|
+
return 120000;
|
|
55224
|
+
const n = Number(raw);
|
|
55225
|
+
return Number.isFinite(n) && n >= 0 ? n : 120000;
|
|
55226
|
+
})();
|
|
55006
55227
|
var TURN_ACTIVE_MARKER_FRESH_MS = 90000;
|
|
55007
55228
|
var AUTOCLASSIFY_MIDTURN_SHADOW = process.env.SWITCHROOM_AUTOCLASSIFY_MIDTURN_SHADOW !== "0";
|
|
55008
55229
|
var lastAgentOutputAt = new Map;
|
|
@@ -55158,11 +55379,12 @@ function hasDifferentThreadedRecentTurn(chatId, liveThreadId) {
|
|
|
55158
55379
|
}
|
|
55159
55380
|
return false;
|
|
55160
55381
|
}
|
|
55161
|
-
function closeObligationOnSubstantiveReply(args, liveTurn) {
|
|
55382
|
+
function closeObligationOnSubstantiveReply(args, liveTurn, routedOriginTurn) {
|
|
55162
55383
|
if (!OBLIGATION_LEDGER_ENABLED)
|
|
55163
55384
|
return;
|
|
55164
55385
|
const echoed = findTurnByOriginId(args.origin_turn_id);
|
|
55165
|
-
const
|
|
55386
|
+
const routedOriginId = routedOriginTurn != null && echoed == null ? routedOriginTurn.turnId : null;
|
|
55387
|
+
const target = obligationLedger.resolveCloseTarget(echoed?.turnId, liveTurn?.turnId, routedOriginId);
|
|
55166
55388
|
if (target != null)
|
|
55167
55389
|
obligationLedger.close(target);
|
|
55168
55390
|
}
|
|
@@ -56538,11 +56760,12 @@ function obligationSweep() {
|
|
|
56538
56760
|
const agent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
56539
56761
|
const now = Date.now();
|
|
56540
56762
|
const backgroundWorkActive = OBLIGATION_BACKGROUND_WORK_GRACE_MS > 0 && agentHasInFlightBackgroundWork(now);
|
|
56541
|
-
const decision = obligationLedger.decideAtIdle(OBLIGATION_ESCALATE_GRACE_MS > 0 || backgroundWorkActive ? {
|
|
56763
|
+
const decision = obligationLedger.decideAtIdle(OBLIGATION_ESCALATE_GRACE_MS > 0 || backgroundWorkActive || OBLIGATION_REPRESENT_GRACE_MS > 0 ? {
|
|
56542
56764
|
now,
|
|
56543
56765
|
graceMs: OBLIGATION_ESCALATE_GRACE_MS,
|
|
56544
56766
|
backgroundWorkActive,
|
|
56545
|
-
backgroundGraceMs: OBLIGATION_BACKGROUND_WORK_GRACE_MS
|
|
56767
|
+
backgroundGraceMs: OBLIGATION_BACKGROUND_WORK_GRACE_MS,
|
|
56768
|
+
representGraceMs: OBLIGATION_REPRESENT_GRACE_MS
|
|
56546
56769
|
} : undefined);
|
|
56547
56770
|
const o = decision.obligation;
|
|
56548
56771
|
if (decision.action === "none" || o == null) {
|
|
@@ -56562,6 +56785,12 @@ function obligationSweep() {
|
|
|
56562
56785
|
`);
|
|
56563
56786
|
return;
|
|
56564
56787
|
}
|
|
56788
|
+
if (HISTORY_ENABLED && hasOutboundDeliveredSince(o.chatId, o.openedAt, o.threadId)) {
|
|
56789
|
+
process.stderr.write(`telegram gateway: obligation closed silently \u2014 outbound delivered since open origin=${o.originTurnId}
|
|
56790
|
+
`);
|
|
56791
|
+
obligationLedger.close(o.originTurnId);
|
|
56792
|
+
return;
|
|
56793
|
+
}
|
|
56565
56794
|
driveEscalation({
|
|
56566
56795
|
escId: o.originTurnId,
|
|
56567
56796
|
inFlight: obligationEscalateInFlight,
|
|
@@ -57253,7 +57482,8 @@ var ALLOWED_TOOLS = new Set([
|
|
|
57253
57482
|
"send_gif",
|
|
57254
57483
|
"vault_request_save",
|
|
57255
57484
|
"vault_request_access",
|
|
57256
|
-
"request_secret"
|
|
57485
|
+
"request_secret",
|
|
57486
|
+
"linear_agent_activity"
|
|
57257
57487
|
]);
|
|
57258
57488
|
async function executeToolCall(tool, args) {
|
|
57259
57489
|
if (!ALLOWED_TOOLS.has(tool)) {
|
|
@@ -57298,6 +57528,8 @@ async function executeToolCall(tool, args) {
|
|
|
57298
57528
|
return executeVaultRequestAccess(args);
|
|
57299
57529
|
case "request_secret":
|
|
57300
57530
|
return executeRequestSecret(args);
|
|
57531
|
+
case "linear_agent_activity":
|
|
57532
|
+
return executeLinearAgentActivity(args);
|
|
57301
57533
|
default:
|
|
57302
57534
|
throw new Error(`unknown tool: ${tool}`);
|
|
57303
57535
|
}
|
|
@@ -57328,6 +57560,9 @@ async function executeSendChecklist(args) {
|
|
|
57328
57560
|
`);
|
|
57329
57561
|
return { content: [{ type: "text", text: `checklist sent (id: ${sent.message_id})` }] };
|
|
57330
57562
|
}
|
|
57563
|
+
async function executeLinearAgentActivity(args) {
|
|
57564
|
+
return emitLinearAgentActivity(args);
|
|
57565
|
+
}
|
|
57331
57566
|
async function executeUpdateChecklist(args) {
|
|
57332
57567
|
const chat_id = args.chat_id;
|
|
57333
57568
|
if (!chat_id)
|
|
@@ -57444,12 +57679,14 @@ ${url}`;
|
|
|
57444
57679
|
effectiveText = text;
|
|
57445
57680
|
}
|
|
57446
57681
|
assertAllowedChat(chat_id);
|
|
57682
|
+
let replyRoutedOriginTurn = null;
|
|
57447
57683
|
let threadId;
|
|
57448
57684
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
57449
57685
|
const explicit = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
57450
57686
|
const echoedTurn = findTurnByOriginId(args.origin_turn_id);
|
|
57451
57687
|
const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(chat_id, args.reply_to) : null;
|
|
57452
57688
|
const originTurn = echoedTurn ?? quotedTurn;
|
|
57689
|
+
replyRoutedOriginTurn = originTurn ?? null;
|
|
57453
57690
|
threadId = resolveAnswerThreadWithLog(chat_id, Number.isFinite(explicit) ? explicit : undefined, originTurn, originTurn == null ? null : echoedTurn != null ? "echo" : "quoted", turn, "reply");
|
|
57454
57691
|
} else {
|
|
57455
57692
|
threadId = resolveThreadId(chat_id, args.message_thread_id ?? (turn?.sessionThreadId != null ? turn.sessionThreadId : undefined));
|
|
@@ -57583,7 +57820,7 @@ ${url}`;
|
|
|
57583
57820
|
disableNotification
|
|
57584
57821
|
});
|
|
57585
57822
|
if (turn2.finalAnswerSubstantive)
|
|
57586
|
-
closeObligationOnSubstantiveReply(args, turn2);
|
|
57823
|
+
closeObligationOnSubstantiveReply(args, turn2, replyRoutedOriginTurn);
|
|
57587
57824
|
}
|
|
57588
57825
|
outboundDedup.record(chat_id, threadId, decision.mergedText, Date.now(), turn2?.registryKey ?? null);
|
|
57589
57826
|
silentAnchorEditDone = true;
|
|
@@ -57786,7 +58023,7 @@ ${url}`;
|
|
|
57786
58023
|
turn.finalAnswerSubstantive = isSubstantiveFinalReply({ text: rawText, disableNotification });
|
|
57787
58024
|
finalizeStatusReaction(chat_id, threadId, "done");
|
|
57788
58025
|
if (turn.finalAnswerSubstantive)
|
|
57789
|
-
closeObligationOnSubstantiveReply(args, turn);
|
|
58026
|
+
closeObligationOnSubstantiveReply(args, turn, replyRoutedOriginTurn);
|
|
57790
58027
|
}
|
|
57791
58028
|
releaseTurnBufferGate(statusKey(chat_id, threadId), turn ?? undefined);
|
|
57792
58029
|
if (turn?.finalAnswerDelivered === true) {
|
|
@@ -57806,13 +58043,19 @@ async function executeStreamReply(args) {
|
|
|
57806
58043
|
throw new Error("stream_reply: chat_id is required");
|
|
57807
58044
|
if (args.text == null || args.text === "")
|
|
57808
58045
|
throw new Error("stream_reply: text is required and cannot be empty");
|
|
58046
|
+
let streamRoutedOriginTurn = null;
|
|
58047
|
+
let streamOriginVia = null;
|
|
58048
|
+
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
58049
|
+
const echoedTurn = findTurnByOriginId(args.origin_turn_id);
|
|
58050
|
+
const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(String(args.chat_id), args.reply_to) : null;
|
|
58051
|
+
const originTurn = echoedTurn ?? quotedTurn;
|
|
58052
|
+
streamRoutedOriginTurn = originTurn ?? null;
|
|
58053
|
+
streamOriginVia = originTurn == null ? null : echoedTurn != null ? "echo" : "quoted";
|
|
58054
|
+
}
|
|
57809
58055
|
if (args.message_thread_id == null) {
|
|
57810
58056
|
let injected;
|
|
57811
58057
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
57812
|
-
|
|
57813
|
-
const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(String(args.chat_id), args.reply_to) : null;
|
|
57814
|
-
const originTurn = echoedTurn ?? quotedTurn;
|
|
57815
|
-
injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, originTurn, originTurn == null ? null : echoedTurn != null ? "echo" : "quoted", turn, "stream_reply");
|
|
58058
|
+
injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, streamRoutedOriginTurn, streamOriginVia, turn, "stream_reply");
|
|
57816
58059
|
} else {
|
|
57817
58060
|
injected = turn?.sessionThreadId;
|
|
57818
58061
|
}
|
|
@@ -57955,7 +58198,7 @@ async function executeStreamReply(args) {
|
|
|
57955
58198
|
done: args.done === true
|
|
57956
58199
|
});
|
|
57957
58200
|
if (turn.finalAnswerSubstantive)
|
|
57958
|
-
closeObligationOnSubstantiveReply(args, turn);
|
|
58201
|
+
closeObligationOnSubstantiveReply(args, turn, streamRoutedOriginTurn);
|
|
57959
58202
|
const streamThreadIdForClear = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
57960
58203
|
clearSilentEndState(statusKey(streamChatId, streamThreadIdForClear));
|
|
57961
58204
|
}
|