switchroom 0.19.45 → 0.19.47

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.
@@ -21547,7 +21547,7 @@ var init_observation_scopes = __esm(() => {
21547
21547
  });
21548
21548
 
21549
21549
  // ../src/config/schema.ts
21550
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
21550
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
21551
21551
  var init_schema = __esm(() => {
21552
21552
  init_zod();
21553
21553
  init_observation_scopes();
@@ -21934,11 +21934,18 @@ var init_schema = __esm(() => {
21934
21934
  "extended",
21935
21935
  "complete"
21936
21936
  ]);
21937
+ GoogleServiceTokenSchema = exports_external.enum([
21938
+ "cal",
21939
+ "drive",
21940
+ "docs",
21941
+ "sheets",
21942
+ "slides"
21943
+ ]);
21937
21944
  GoogleWorkspaceConfigSchema = exports_external.object({
21938
21945
  google_client_id: exports_external.string().min(1).describe("Google OAuth client ID (literal string or vault reference e.g. 'vault:google-oauth-client-id')"),
21939
21946
  google_client_secret: exports_external.string().min(1).describe("Google OAuth client secret (literal string or vault reference e.g. 'vault:google-oauth-client-secret')"),
21940
21947
  approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified."),
21941
- tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet \u2014 see RFC G \u00a75).")
21948
+ tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar tools; the " + "Calendar tools only authenticate when the account holds the opt-in " + "calendar scope \u2014 no tier mints it). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet \u2014 see RFC G \u00a75).")
21942
21949
  }).optional();
21943
21950
  LiteLLMConfigSchema = exports_external.object({
21944
21951
  enabled: exports_external.boolean().optional().describe("Opt-in toggle. When true, `switchroom apply` provisions a per-agent " + "LiteLLM virtual key and injects routing env into the container. " + "Default OFF."),
@@ -21971,7 +21978,7 @@ var init_schema = __esm(() => {
21971
21978
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
21972
21979
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
21973
21980
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
21974
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
21981
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
21975
21982
  });
21976
21983
  MicrosoftWorkspaceConfigSchema = exports_external.object({
21977
21984
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -22445,7 +22452,9 @@ var init_schema = __esm(() => {
22445
22452
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
22446
22453
  enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
22447
22454
  message: "Agent name must match the standard agent-name pattern"
22448
- })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer \u2014 " + "the agent still authenticates via socket-path-as-identity " + "per RFC D \u00a74.1, broker just gates the cross-agent token share.")
22455
+ })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer \u2014 " + "the agent still authenticates via socket-path-as-identity " + "per RFC D \u00a74.1, broker just gates the cross-agent token share."),
22456
+ readonly: exports_external.boolean().optional().describe("Per-account read-only selection (v1 scope model). true \u2192 the " + "token minted by `auth google account add` carries ONLY " + ".readonly scope variants (zero write scopes), and the gdrive " + "MCP launcher passes upstream `--read-only` so write tools are " + "not exposed. Written by `account add --readonly`; re-read on " + "`--replace` so a re-consent cannot silently re-widen. Omitted " + "= legacy behaviour (tier-tied read-write document scopes)."),
22457
+ services: exports_external.array(GoogleServiceTokenSchema).min(1).optional().describe("Per-account service selection (v1 scope model). Which Google " + "services the minted token covers AND the gdrive MCP exposes " + "(upstream `--tools`): cal, drive, docs, sheets, slides. " + "Written by `account add --services`; re-read on `--replace`. " + "Omitted = the tier's default services (drive,docs,sheets; " + "+slides at extended/complete).")
22449
22458
  })).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email \u2192 list of agents " + "permitted to read that account's slots. Written by `switchroom " + "auth google enable|disable` (Phase 3); read by the broker on " + "every Google slot access. Replaces RFC D's per-agent vault slot " + "scope (which can't express 'two agents share one Google account')."),
22450
22459
  microsoft_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
22451
22460
  message: "Account key must be a Microsoft account email like 'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
@@ -54129,8 +54138,9 @@ function createHandbackPreturnSignal(deps) {
54129
54138
  entry.reapTimer = setTimer(() => reap(entry), adoptTimeoutMs);
54130
54139
  return;
54131
54140
  }
54141
+ const consumedElsewhere = deps.sessionActivitySince?.(entry.deliveredAt) === true;
54132
54142
  entry.consumed = true;
54133
- stopTypingUnlessTurnLive(entry, "orphan reap");
54143
+ stopTypingUnlessTurnLive(entry, consumedElsewhere ? "consumed reap" : "orphan reap");
54134
54144
  if (entry.activityMessageId != null) {
54135
54145
  const record = {
54136
54146
  turnKey: entry.syntheticTurnKey,
@@ -54147,6 +54157,11 @@ function createHandbackPreturnSignal(deps) {
54147
54157
  });
54148
54158
  }
54149
54159
  dropEntry(entry);
54160
+ if (consumedElsewhere) {
54161
+ log(`handback-preturn-signal: reap resolved key=${entry.statusKey} ` + `turnId=${entry.adoptTurnId} \u2014 session consumed input after delivery ` + `(no adopting turn observed); cleaned up without re-injection
54162
+ `);
54163
+ return;
54164
+ }
54150
54165
  if (entry.reinjectCount < maxReinjects) {
54151
54166
  const nextCount = entry.reinjectCount + 1;
54152
54167
  entry.inbound.meta[HANDBACK_REINJECT_COUNT_META_KEY] = String(nextCount);
@@ -54296,6 +54311,28 @@ function createHandbackPreturnSignal(deps) {
54296
54311
  };
54297
54312
  }
54298
54313
 
54314
+ // gateway/session-consume-signal.ts
54315
+ class SessionConsumeSignal {
54316
+ lastTurnMintAt = 0;
54317
+ lastMainToolCallAt = 0;
54318
+ noteTurnMint(now = Date.now()) {
54319
+ if (now > this.lastTurnMintAt)
54320
+ this.lastTurnMintAt = now;
54321
+ }
54322
+ noteMainToolCall(now = Date.now()) {
54323
+ if (now > this.lastMainToolCallAt)
54324
+ this.lastMainToolCallAt = now;
54325
+ }
54326
+ activitySince(sinceTs) {
54327
+ return this.lastTurnMintAt > sinceTs || this.lastMainToolCallAt > sinceTs;
54328
+ }
54329
+ reset() {
54330
+ this.lastTurnMintAt = 0;
54331
+ this.lastMainToolCallAt = 0;
54332
+ }
54333
+ }
54334
+ var sessionConsumeSignal = new SessionConsumeSignal;
54335
+
54299
54336
  // gateway/handback-orphan-recovery.ts
54300
54337
  function formatOrphanEscalation(esc) {
54301
54338
  return `telegram gateway: handback orphan escalation key=${esc.statusKey} ` + `turnId=${esc.adoptTurnId} reinjects=${esc.reinjectCount} ageMs=${esc.ageMs} ` + `\u2014 deterministic recovery exhausted, no operator nudge issued
@@ -72781,7 +72818,7 @@ function parseProviderVerb(provider, rest) {
72781
72818
  return { kind: "provider-cancel", provider };
72782
72819
  }
72783
72820
  if (sub !== "add") {
72784
- const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
72821
+ const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write] [--calendar]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
72785
72822
  return {
72786
72823
  kind: "help",
72787
72824
  reason: `Unknown \`${provider}\` subcommand: \`${codeSpanSafe(sub || "(none)")}\`. ${usage}`
@@ -72792,13 +72829,13 @@ function parseProviderVerb(provider, rest) {
72792
72829
  const positional = args.filter((a) => !a.startsWith("--"));
72793
72830
  const email = positional[0];
72794
72831
  if (!email) {
72795
- const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
72832
+ const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write] [--calendar]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
72796
72833
  return { kind: "help", reason: usage };
72797
72834
  }
72798
72835
  const emailErr = validateAuthAddLabel(email);
72799
72836
  if (emailErr)
72800
72837
  return { kind: "help", reason: emailErr };
72801
- const allowed = provider === "google" ? new Set(["--replace", "--write"]) : new Set(["--replace", "--org-mode"]);
72838
+ const allowed = provider === "google" ? new Set(["--replace", "--write", "--calendar"]) : new Set(["--replace", "--org-mode"]);
72802
72839
  for (const f of flags) {
72803
72840
  if (!allowed.has(f)) {
72804
72841
  return {
@@ -72813,6 +72850,7 @@ function parseProviderVerb(provider, rest) {
72813
72850
  email,
72814
72851
  replace: flags.has("--replace"),
72815
72852
  write: provider === "google" && flags.has("--write"),
72853
+ calendar: provider === "google" && flags.has("--calendar"),
72816
72854
  orgMode: provider === "microsoft" && flags.has("--org-mode")
72817
72855
  };
72818
72856
  }
@@ -74520,16 +74558,16 @@ ${loginUrl}
74520
74558
 
74521
74559
  // gateway/auth-loopback-relay.ts
74522
74560
  import { spawn } from "node:child_process";
74523
- function defaultSpawnRelay(provider, email, opts) {
74524
- const binary = opts.binary ?? "switchroom";
74525
- const args = provider === "google" ? [
74561
+ function buildRelayArgs(provider, email, opts) {
74562
+ return provider === "google" ? [
74526
74563
  "auth",
74527
74564
  "google",
74528
74565
  "account",
74529
74566
  "add",
74530
74567
  email,
74531
74568
  ...opts.replace ? ["--replace"] : [],
74532
- ...opts.write ? ["--write"] : []
74569
+ ...opts.write ? ["--write"] : [],
74570
+ ...opts.calendar ? ["--calendar"] : []
74533
74571
  ] : [
74534
74572
  "auth",
74535
74573
  "microsoft",
@@ -74539,6 +74577,10 @@ function defaultSpawnRelay(provider, email, opts) {
74539
74577
  ...opts.replace ? ["--replace"] : [],
74540
74578
  ...opts.orgMode ? ["--org-mode"] : []
74541
74579
  ];
74580
+ }
74581
+ function defaultSpawnRelay(provider, email, opts) {
74582
+ const binary = opts.binary ?? "switchroom";
74583
+ const args = buildRelayArgs(provider, email, opts);
74542
74584
  const child = spawn(binary, args, {
74543
74585
  stdio: ["ignore", "pipe", "pipe"],
74544
74586
  env: {
@@ -80369,6 +80411,28 @@ class SubagentReplyAuthority2 {
80369
80411
  }
80370
80412
  var subagentReplyAuthority2 = new SubagentReplyAuthority2;
80371
80413
 
80414
+ // gateway/session-consume-signal.ts
80415
+ class SessionConsumeSignal2 {
80416
+ lastTurnMintAt = 0;
80417
+ lastMainToolCallAt = 0;
80418
+ noteTurnMint(now = Date.now()) {
80419
+ if (now > this.lastTurnMintAt)
80420
+ this.lastTurnMintAt = now;
80421
+ }
80422
+ noteMainToolCall(now = Date.now()) {
80423
+ if (now > this.lastMainToolCallAt)
80424
+ this.lastMainToolCallAt = now;
80425
+ }
80426
+ activitySince(sinceTs) {
80427
+ return this.lastTurnMintAt > sinceTs || this.lastMainToolCallAt > sinceTs;
80428
+ }
80429
+ reset() {
80430
+ this.lastTurnMintAt = 0;
80431
+ this.lastMainToolCallAt = 0;
80432
+ }
80433
+ }
80434
+ var sessionConsumeSignal2 = new SessionConsumeSignal2;
80435
+
80372
80436
  // gateway/feed-open-gate.ts
80373
80437
  function shouldEarlyOpenLiveness(input) {
80374
80438
  if (!input.enabled)
@@ -81296,6 +81360,7 @@ function beginTurn(deps, ev) {
81296
81360
  scheduleEarlyLivenessOpen(next);
81297
81361
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
81298
81362
  `);
81363
+ sessionConsumeSignal2.noteTurnMint(startedAt);
81299
81364
  rememberRecentTurn(next);
81300
81365
  promoteQueuedStatus(ev.chatId, enqThreadIdNum);
81301
81366
  shadowEmit({
@@ -83008,6 +83073,18 @@ class SubagentHandbackMarker2 {
83008
83073
 
83009
83074
  // answer-ready-flush.ts
83010
83075
  var ANSWER_READY_FLUSH_MS = 1000;
83076
+ var ANSWER_STAGE_MS = 30000;
83077
+ function resolveAnswerStageMs(env) {
83078
+ const raw = env.SWITCHROOM_ANSWER_STAGE_MS;
83079
+ if (raw == null || raw.trim() === "")
83080
+ return ANSWER_STAGE_MS;
83081
+ const n = Number(raw);
83082
+ if (!Number.isFinite(n))
83083
+ return ANSWER_STAGE_MS;
83084
+ if (n <= 0)
83085
+ return 0;
83086
+ return Math.floor(n);
83087
+ }
83011
83088
  function resolveAnswerReadyFlushMs(env) {
83012
83089
  const raw = env.SWITCHROOM_ANSWER_READY_FLUSH_MS;
83013
83090
  if (raw == null || raw.trim() === "")
@@ -83034,6 +83111,7 @@ class AnswerReadyFlushController {
83034
83111
  constructor(deps) {
83035
83112
  this.deps = deps;
83036
83113
  }
83114
+ staged = new WeakSet;
83037
83115
  get setTimeoutFn() {
83038
83116
  return this.deps.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
83039
83117
  }
@@ -83043,12 +83121,16 @@ class AnswerReadyFlushController {
83043
83121
  clear(turn) {
83044
83122
  if (turn == null)
83045
83123
  return;
83124
+ this.staged.delete(turn);
83046
83125
  const handle = this.deps.getTimerHandle(turn);
83047
83126
  if (handle != null) {
83048
83127
  this.clearTimeoutFn(handle);
83049
83128
  this.deps.setTimerHandle(turn, null);
83050
83129
  }
83051
83130
  }
83131
+ isStaged(turn) {
83132
+ return this.staged.has(turn);
83133
+ }
83052
83134
  reset() {
83053
83135
  const turn = this.deps.getCurrentTurn();
83054
83136
  this.clear(turn);
@@ -83065,9 +83147,25 @@ class AnswerReadyFlushController {
83065
83147
  if (live == null || live !== armedTurn)
83066
83148
  return;
83067
83149
  this.deps.setTimerHandle(live, null);
83068
- if (!shouldArmAnswerReadyFlush(this.deps.getArmInput(live)))
83150
+ if (!shouldArmAnswerReadyFlush(this.deps.getArmInput(live))) {
83151
+ if (this.staged.delete(live)) {
83152
+ this.deps.log?.("answer-ready stage discarded \u2014 reply/tool activity landed inside the completion window");
83153
+ }
83069
83154
  return;
83070
- this.deps.log?.("answer-ready quiescence flush \u2014 delivering composed terminal answer");
83155
+ }
83156
+ const stageMs = this.deps.stageWindowMs ?? 0;
83157
+ if (stageMs > 0 && !this.staged.has(live)) {
83158
+ this.staged.add(live);
83159
+ this.deps.log?.(`answer-ready quiescence \u2014 staging composed terminal answer ` + `(promotes in ${stageMs}ms unless the reply lands or the turn completes)`);
83160
+ const handle = this.setTimeoutFn(() => this.onExpiry(live), stageMs);
83161
+ this.deps.setTimerHandle(live, handle);
83162
+ return;
83163
+ }
83164
+ if (this.staged.delete(live)) {
83165
+ this.deps.log?.("answer-ready stage promoted \u2014 completion window closed with no reply; delivering composed terminal answer");
83166
+ } else {
83167
+ this.deps.log?.("answer-ready quiescence flush \u2014 delivering composed terminal answer");
83168
+ }
83071
83169
  this.deps.onFlush(live);
83072
83170
  }
83073
83171
  }
@@ -86166,7 +86264,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
86166
86264
  "HINDSIGHT_MCP_RECALL_BUDGET_MODE",
86167
86265
  "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT",
86168
86266
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
86169
- "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT"
86267
+ "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
86268
+ "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S"
86170
86269
  ]);
86171
86270
  var HINDSIGHT_WORKER_SLOT_TYPES = [
86172
86271
  "consolidation",
@@ -101041,10 +101140,10 @@ function startOutboxSweep(deps) {
101041
101140
  }
101042
101141
 
101043
101142
  // ../src/build-info.ts
101044
- var VERSION2 = "0.19.45";
101045
- var COMMIT_SHA = "2ad33d5d";
101046
- var COMMIT_DATE = "2026-08-01T23:47:32Z";
101047
- var LATEST_PR = 4192;
101143
+ var VERSION2 = "0.19.47";
101144
+ var COMMIT_SHA = "9693493e";
101145
+ var COMMIT_DATE = "2026-08-02T01:20:29Z";
101146
+ var LATEST_PR = 4201;
101048
101147
  var COMMITS_AHEAD_OF_TAG = 0;
101049
101148
 
101050
101149
  // gateway/boot-version.ts
@@ -107076,6 +107175,8 @@ if (isGatewayMain)
107076
107175
  async onToolCall(client3, msg) {
107077
107176
  process.stderr.write(`telegram gateway: ipc: tool_call tool=${msg.tool} agent=${client3.agentName ?? "-"} clientId=${client3.id ?? "-"} callId=${msg.id}
107078
107177
  `);
107178
+ if (!isCronIdentity(client3.agentName))
107179
+ sessionConsumeSignal.noteMainToolCall();
107079
107180
  try {
107080
107181
  const result = await executeToolCall(msg.tool, msg.args, client3.agentName);
107081
107182
  return { type: "tool_call_result", id: msg.id, success: true, result };
@@ -108764,6 +108865,7 @@ var answerReadyFlush = new AnswerReadyFlushController({
108764
108865
  turn.answerReadyFlushTimeoutId = handle;
108765
108866
  },
108766
108867
  onFlush: () => handleSessionEvent2({ kind: "turn_end", durationMs: -1, reason: "answer-ready-quiescence" }),
108868
+ stageWindowMs: resolveAnswerStageMs(process.env),
108767
108869
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}
108768
108870
  `)
108769
108871
  });
@@ -108902,7 +109004,8 @@ var handbackPreturnSignal = createHandbackPreturnSignal({
108902
109004
  return false;
108903
109005
  return statusKey(live.sessionChatId, live.sessionThreadId) === key;
108904
109006
  },
108905
- isClaudeBusy: () => isMachineInTurn()
109007
+ isClaudeBusy: () => isMachineInTurn(),
109008
+ sessionActivitySince: (sinceTs) => sessionConsumeSignal.activitySince(sinceTs)
108906
109009
  });
108907
109010
  var memoryLegibilityStager = new MemoryLegibilityStager;
108908
109011
  function sendMemoryLegibilityLine(event, chatId, threadId) {
@@ -112527,7 +112630,7 @@ Set \`admin: true\` on this agent in switchroom.yaml to unlock.`, { html: true }
112527
112630
  return;
112528
112631
  }
112529
112632
  try {
112530
- const { consentUrl, state: state7, port, child } = await startLoopbackFlow(parsed.provider, parsed.email, { replace: parsed.replace, write: parsed.write, orgMode: parsed.orgMode });
112633
+ const { consentUrl, state: state7, port, child } = await startLoopbackFlow(parsed.provider, parsed.email, { replace: parsed.replace, write: parsed.write, calendar: parsed.calendar, orgMode: parsed.orgMode });
112531
112634
  const newFlow = {
112532
112635
  provider: parsed.provider,
112533
112636
  email: parsed.email,
@@ -65,6 +65,8 @@ export type ParsedAuthCommand =
65
65
  replace: boolean
66
66
  /** Google `--write` (Drive write scope). */
67
67
  write: boolean
68
+ /** Google `--calendar` (read-only Calendar scope). */
69
+ calendar: boolean
68
70
  /** Microsoft `--org-mode`. */
69
71
  orgMode: boolean
70
72
  }
@@ -251,7 +253,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
251
253
  * wired; anything else is a help-with-reason so the operator sees the shape.
252
254
  *
253
255
  * Flags mirror the CLI:
254
- * google: `add <email> [--replace] [--write]`
256
+ * google: `add <email> [--replace] [--write] [--calendar]`
255
257
  * microsoft: `add <email> [--replace] [--org-mode]`
256
258
  */
257
259
  export function parseProviderVerb(
@@ -265,7 +267,7 @@ export function parseProviderVerb(
265
267
  if (sub !== 'add') {
266
268
  const usage =
267
269
  provider === 'google'
268
- ? 'Usage: /auth google add <email> [--replace] [--write]'
270
+ ? 'Usage: /auth google add <email> [--replace] [--write] [--calendar]'
269
271
  : 'Usage: /auth microsoft add <email> [--replace] [--org-mode]'
270
272
  return {
271
273
  kind: 'help',
@@ -280,7 +282,7 @@ export function parseProviderVerb(
280
282
  if (!email) {
281
283
  const usage =
282
284
  provider === 'google'
283
- ? 'Usage: /auth google add <email> [--replace] [--write]'
285
+ ? 'Usage: /auth google add <email> [--replace] [--write] [--calendar]'
284
286
  : 'Usage: /auth microsoft add <email> [--replace] [--org-mode]'
285
287
  return { kind: 'help', reason: usage }
286
288
  }
@@ -288,7 +290,7 @@ export function parseProviderVerb(
288
290
  if (emailErr) return { kind: 'help', reason: emailErr }
289
291
  // Reject unknown flags so a typo (`--replaced`) isn't silently dropped.
290
292
  const allowed = provider === 'google'
291
- ? new Set(['--replace', '--write'])
293
+ ? new Set(['--replace', '--write', '--calendar'])
292
294
  : new Set(['--replace', '--org-mode'])
293
295
  for (const f of flags) {
294
296
  if (!allowed.has(f)) {
@@ -304,6 +306,7 @@ export function parseProviderVerb(
304
306
  email,
305
307
  replace: flags.has('--replace'),
306
308
  write: provider === 'google' && flags.has('--write'),
309
+ calendar: provider === 'google' && flags.has('--calendar'),
307
310
  orgMode: provider === 'microsoft' && flags.has('--org-mode'),
308
311
  }
309
312
  }
@@ -84,6 +84,8 @@ export interface SpawnRelayOpts {
84
84
  replace?: boolean
85
85
  /** Google `--write` (Drive write scope). */
86
86
  write?: boolean
87
+ /** Google `--calendar` (read-only Calendar scope). */
88
+ calendar?: boolean
87
89
  /** Microsoft `--org-mode`. */
88
90
  orgMode?: boolean
89
91
  /** Override the CLI binary (tests / non-default install). */
@@ -96,6 +98,42 @@ export type SpawnRelay = (
96
98
  opts: SpawnRelayOpts,
97
99
  ) => RelayChild
98
100
 
101
+ /**
102
+ * Build the `switchroom auth <provider> account add` argv the relay spawns.
103
+ *
104
+ * Pure + exported so the flag plumbing is unit-pinned: every opt-in flag
105
+ * the chat command accepts has to actually reach the CLI, and a flag that
106
+ * was NOT requested must never appear (the relay is the only path most
107
+ * operators use, so a dropped `--calendar` here is indistinguishable from
108
+ * the feature not existing, and a spurious one silently widens a grant).
109
+ */
110
+ export function buildRelayArgs(
111
+ provider: LoopbackProvider,
112
+ email: string,
113
+ opts: SpawnRelayOpts,
114
+ ): string[] {
115
+ return provider === 'google'
116
+ ? [
117
+ 'auth',
118
+ 'google',
119
+ 'account',
120
+ 'add',
121
+ email,
122
+ ...(opts.replace ? ['--replace'] : []),
123
+ ...(opts.write ? ['--write'] : []),
124
+ ...(opts.calendar ? ['--calendar'] : []),
125
+ ]
126
+ : [
127
+ 'auth',
128
+ 'microsoft',
129
+ 'account',
130
+ 'add',
131
+ email,
132
+ ...(opts.replace ? ['--replace'] : []),
133
+ ...(opts.orgMode ? ['--org-mode'] : []),
134
+ ]
135
+ }
136
+
99
137
  /**
100
138
  * Default spawn: the real `switchroom` CLI, stdout+stderr piped, stdin
101
139
  * ignored. `BROWSER=/bin/true` suppresses any browser auto-open; the Drive
@@ -109,26 +147,7 @@ export function defaultSpawnRelay(
109
147
  opts: SpawnRelayOpts,
110
148
  ): RelayChild {
111
149
  const binary = opts.binary ?? 'switchroom'
112
- const args =
113
- provider === 'google'
114
- ? [
115
- 'auth',
116
- 'google',
117
- 'account',
118
- 'add',
119
- email,
120
- ...(opts.replace ? ['--replace'] : []),
121
- ...(opts.write ? ['--write'] : []),
122
- ]
123
- : [
124
- 'auth',
125
- 'microsoft',
126
- 'account',
127
- 'add',
128
- email,
129
- ...(opts.replace ? ['--replace'] : []),
130
- ...(opts.orgMode ? ['--org-mode'] : []),
131
- ]
150
+ const args = buildRelayArgs(provider, email, opts)
132
151
  const child: ChildProcess = spawn(binary, args, {
133
152
  stdio: ['ignore', 'pipe', 'pipe'],
134
153
  env: {
@@ -283,6 +283,7 @@ import { NarrativeFlushController, PENDING_NARRATIVE_FLUSH_MS } from '../narrati
283
283
  import { createTypingWrapper } from '../typing-wrap.js'
284
284
  import { createTurnTypingLoop } from './turn-typing-loop.js'
285
285
  import { createHandbackPreturnSignal } from './handback-preturn-signal.js'
286
+ import { sessionConsumeSignal } from './session-consume-signal.js'
286
287
  import { createHandbackOrphanRecovery } from './handback-orphan-recovery.js'
287
288
  import { deriveTurnId } from './derive-turn-id.js'
288
289
  import { createTypingEmitter, TYPING_REFRESH_MS } from '../typing-emitter.js'
@@ -495,10 +496,7 @@ import {
495
496
  } from '../reply-owner-resolve.js'
496
497
  import { SubagentHandbackMarker } from './subagent-handback-marker.js'
497
498
  // PR A — deterministic answer-ready quiescence flush (late-delivery fix).
498
- import {
499
- AnswerReadyFlushController,
500
- resolveAnswerReadyFlushMs,
501
- } from '../answer-ready-flush.js'
499
+ import { AnswerReadyFlushController, resolveAnswerReadyFlushMs, resolveAnswerStageMs } from '../answer-ready-flush.js'
502
500
  // #1667 — pure decision core for the turn_end answer-delivery gate (#1664).
503
501
  import { decideTurnEndGate } from './turn-end-gate.js'
504
502
  // #1122 PR3: turn-flush-prose-recovery removed with the progress card.
@@ -10748,6 +10746,7 @@ if (isGatewayMain) ipcServer = createIpcServer({
10748
10746
 
10749
10747
  async onToolCall(client: IpcClient, msg: ToolCallMessage): Promise<ToolCallResult> {
10750
10748
  process.stderr.write(`telegram gateway: ipc: tool_call tool=${msg.tool} agent=${client.agentName ?? '-'} clientId=${client.id ?? '-'} callId=${msg.id}\n`)
10749
+ if (!isCronIdentity(client.agentName)) sessionConsumeSignal.noteMainToolCall() // consumption proxy (session-consume-signal.ts)
10751
10750
  try {
10752
10751
  // #4172 — the calling client's identity gates reply supersede authority
10753
10752
  // (see replyCallerIsForeignSession, cron-session.ts).
@@ -13547,29 +13546,29 @@ function resetOrphanedReplyTimeout(): void {
13547
13546
  * PR A — DETERMINISTIC answer-ready quiescence flush controller.
13548
13547
  *
13549
13548
  * The orchestration (arm / debounce / rollover-guard / fire-time re-verify /
13550
- * disarm) lives in the extracted, unit-tested `AnswerReadyFlushController`
13551
- * (answer-ready-flush.ts) — the gateway only supplies the thin deps below and
13552
- * calls `.reset()` / `.clear(turn)`. Behaviour:
13549
+ * stage-don't-send / disarm) lives in the extracted, unit-tested
13550
+ * `AnswerReadyFlushController` (answer-ready-flush.ts) — the gateway only
13551
+ * supplies the thin deps below and calls `.reset()` / `.clear(turn)`.
13553
13552
  *
13554
13553
  * - `reset()` (from `case 'text'`): (re)arm iff the turn has a genuine composed
13555
13554
  * terminal answer (the SAME `decideTurnFlush` classifier the turn-flush branch
13556
13555
  * uses) AND is quiescent. Each text chunk re-arms → the debounce.
13557
13556
  * - `clear(turn)` (from tool activity + `endCurrentTurnAtomic`): the DISARM.
13558
- * - on fire: re-pin `currentTurn === turn`, re-verify quiescence, then dispatch
13559
- * a positive `answer-ready-quiescence` synthetic turn_end that routes through
13560
- * the IDENTICAL turn-flush send path (endCurrentTurnAtomic send-gated IIFE
13561
- * honest PR-B record). Replaces the ~150 s dead wait with ~1 s, in code.
13557
+ * - on fire: re-pin `currentTurn === turn`, re-verify quiescence, then STAGE the
13558
+ * answer on the still-live turn (`stageWindowMs` see ANSWER_STAGE_MS) and
13559
+ * promote only when the completion window closes with no reply, dispatching
13560
+ * the positive `answer-ready-quiescence` synthetic turn_end through the
13561
+ * IDENTICAL turn-flush send path. A reply landing in the window discards the
13562
+ * stage (msgs 25843/25844); a real turn_end delivers via the turn-flush branch.
13562
13563
  *
13563
13564
  * Exactly-once: the synthetic turn_end's `endCurrentTurnAtomic` nulls the atom,
13564
13565
  * so a later REAL turn_end (or the orphaned backstop) short-circuits at its
13565
13566
  * `turn != null` guard; `endCurrentTurnAtomic` also calls `.clear(turn)` so a
13566
- * real turn_end that lands FIRST cancels a pending flush. `outboundDedup` is the
13567
- * second layer.
13567
+ * real turn_end that lands FIRST cancels a pending flush/stage.
13568
13568
  *
13569
13569
  * The `answer-ready-quiescence` reason bypasses the `durationMs===-1`
13570
- * recently-streaming SUPPRESSION guard (the terminal answer text itself stamps
13571
- * recentlyStreaming): quiescence IS the positive "streaming has settled" signal,
13572
- * the opposite of the hung-turn backstop that guard protects.
13570
+ * recently-streaming SUPPRESSION guard: quiescence IS the positive "streaming
13571
+ * has settled" signal, the opposite of the hung-turn backstop that guard covers.
13573
13572
  */
13574
13573
  const answerReadyFlush = new AnswerReadyFlushController<CurrentTurn>({
13575
13574
  getCurrentTurn: () => currentTurn,
@@ -13593,6 +13592,7 @@ const answerReadyFlush = new AnswerReadyFlushController<CurrentTurn>({
13593
13592
  },
13594
13593
  onFlush: () =>
13595
13594
  handleSessionEvent({ kind: 'turn_end', durationMs: -1, reason: 'answer-ready-quiescence' }),
13595
+ stageWindowMs: resolveAnswerStageMs(process.env), // stage-don't-send — see ANSWER_STAGE_MS
13596
13596
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}\n`),
13597
13597
  })
13598
13598
 
@@ -13831,9 +13831,10 @@ const handbackPreturnSignal = createHandbackPreturnSignal({
13831
13831
  return statusKey(live.sessionChatId, live.sessionThreadId) === key
13832
13832
  },
13833
13833
  // Queue-state gate for the orphan reap (see the signal's `isClaudeBusy` doc):
13834
- // while a turn is in flight the handback is enqueued behind it, not orphaned.
13835
- // False for both `bridge_alive_idle` and `bridge_dead` → genuine orphan reaps.
13834
+ // a handback queued behind an in-flight turn is not orphaned.
13836
13835
  isClaudeBusy: () => isMachineInTurn(),
13836
+ // 2026-08-02 invisible-consumption gate — see session-consume-signal.ts.
13837
+ sessionActivitySince: (sinceTs) => sessionConsumeSignal.activitySince(sinceTs),
13837
13838
  })
13838
13839
 
13839
13840
  /**
@@ -20738,7 +20739,7 @@ bot.command("auth", async ctx => {
20738
20739
  const { consentUrl, state, port, child } = await startLoopbackFlow(
20739
20740
  parsed.provider,
20740
20741
  parsed.email,
20741
- { replace: parsed.replace, write: parsed.write, orgMode: parsed.orgMode },
20742
+ { replace: parsed.replace, write: parsed.write, calendar: parsed.calendar, orgMode: parsed.orgMode },
20742
20743
  )
20743
20744
  const newFlow = {
20744
20745
  provider: parsed.provider,