switchroom 0.19.46 → 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.
- package/dist/agent-scheduler/index.js +12 -3
- package/dist/auth-broker/index.js +15 -5
- package/dist/cli/notion-write-pretool.mjs +12 -3
- package/dist/cli/switchroom.js +400 -48
- package/dist/host-control/main.js +16 -6
- package/dist/vault/approvals/kernel-server.js +15 -5
- package/dist/vault/broker/server.js +15 -5
- package/package.json +1 -1
- package/telegram-plugin/answer-ready-flush.ts +118 -6
- package/telegram-plugin/dist/gateway/gateway.js +111 -13
- package/telegram-plugin/gateway/gateway.ts +19 -18
- package/telegram-plugin/gateway/handback-preturn-signal.ts +44 -6
- package/telegram-plugin/gateway/session-consume-signal.ts +72 -0
- package/telegram-plugin/gateway/stream-render.ts +5 -0
- package/telegram-plugin/tests/answer-ready-flush.test.ts +147 -3
- package/telegram-plugin/tests/handback-preturn-signal.test.ts +77 -0
|
@@ -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
|
|
@@ -80374,6 +80411,28 @@ class SubagentReplyAuthority2 {
|
|
|
80374
80411
|
}
|
|
80375
80412
|
var subagentReplyAuthority2 = new SubagentReplyAuthority2;
|
|
80376
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
|
+
|
|
80377
80436
|
// gateway/feed-open-gate.ts
|
|
80378
80437
|
function shouldEarlyOpenLiveness(input) {
|
|
80379
80438
|
if (!input.enabled)
|
|
@@ -81301,6 +81360,7 @@ function beginTurn(deps, ev) {
|
|
|
81301
81360
|
scheduleEarlyLivenessOpen(next);
|
|
81302
81361
|
process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
|
|
81303
81362
|
`);
|
|
81363
|
+
sessionConsumeSignal2.noteTurnMint(startedAt);
|
|
81304
81364
|
rememberRecentTurn(next);
|
|
81305
81365
|
promoteQueuedStatus(ev.chatId, enqThreadIdNum);
|
|
81306
81366
|
shadowEmit({
|
|
@@ -83013,6 +83073,18 @@ class SubagentHandbackMarker2 {
|
|
|
83013
83073
|
|
|
83014
83074
|
// answer-ready-flush.ts
|
|
83015
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
|
+
}
|
|
83016
83088
|
function resolveAnswerReadyFlushMs(env) {
|
|
83017
83089
|
const raw = env.SWITCHROOM_ANSWER_READY_FLUSH_MS;
|
|
83018
83090
|
if (raw == null || raw.trim() === "")
|
|
@@ -83039,6 +83111,7 @@ class AnswerReadyFlushController {
|
|
|
83039
83111
|
constructor(deps) {
|
|
83040
83112
|
this.deps = deps;
|
|
83041
83113
|
}
|
|
83114
|
+
staged = new WeakSet;
|
|
83042
83115
|
get setTimeoutFn() {
|
|
83043
83116
|
return this.deps.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
|
|
83044
83117
|
}
|
|
@@ -83048,12 +83121,16 @@ class AnswerReadyFlushController {
|
|
|
83048
83121
|
clear(turn) {
|
|
83049
83122
|
if (turn == null)
|
|
83050
83123
|
return;
|
|
83124
|
+
this.staged.delete(turn);
|
|
83051
83125
|
const handle = this.deps.getTimerHandle(turn);
|
|
83052
83126
|
if (handle != null) {
|
|
83053
83127
|
this.clearTimeoutFn(handle);
|
|
83054
83128
|
this.deps.setTimerHandle(turn, null);
|
|
83055
83129
|
}
|
|
83056
83130
|
}
|
|
83131
|
+
isStaged(turn) {
|
|
83132
|
+
return this.staged.has(turn);
|
|
83133
|
+
}
|
|
83057
83134
|
reset() {
|
|
83058
83135
|
const turn = this.deps.getCurrentTurn();
|
|
83059
83136
|
this.clear(turn);
|
|
@@ -83070,9 +83147,25 @@ class AnswerReadyFlushController {
|
|
|
83070
83147
|
if (live == null || live !== armedTurn)
|
|
83071
83148
|
return;
|
|
83072
83149
|
this.deps.setTimerHandle(live, null);
|
|
83073
|
-
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
|
+
}
|
|
83154
|
+
return;
|
|
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);
|
|
83074
83162
|
return;
|
|
83075
|
-
|
|
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
|
+
}
|
|
83076
83169
|
this.deps.onFlush(live);
|
|
83077
83170
|
}
|
|
83078
83171
|
}
|
|
@@ -86171,7 +86264,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
|
|
|
86171
86264
|
"HINDSIGHT_MCP_RECALL_BUDGET_MODE",
|
|
86172
86265
|
"HINDSIGHT_API_LLM_TEMPERATURE_REFLECT",
|
|
86173
86266
|
"HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
|
|
86174
|
-
"HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT"
|
|
86267
|
+
"HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
|
|
86268
|
+
"HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S"
|
|
86175
86269
|
]);
|
|
86176
86270
|
var HINDSIGHT_WORKER_SLOT_TYPES = [
|
|
86177
86271
|
"consolidation",
|
|
@@ -101046,10 +101140,10 @@ function startOutboxSweep(deps) {
|
|
|
101046
101140
|
}
|
|
101047
101141
|
|
|
101048
101142
|
// ../src/build-info.ts
|
|
101049
|
-
var VERSION2 = "0.19.
|
|
101050
|
-
var COMMIT_SHA = "
|
|
101051
|
-
var COMMIT_DATE = "2026-08-
|
|
101052
|
-
var LATEST_PR =
|
|
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;
|
|
101053
101147
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
101054
101148
|
|
|
101055
101149
|
// gateway/boot-version.ts
|
|
@@ -107081,6 +107175,8 @@ if (isGatewayMain)
|
|
|
107081
107175
|
async onToolCall(client3, msg) {
|
|
107082
107176
|
process.stderr.write(`telegram gateway: ipc: tool_call tool=${msg.tool} agent=${client3.agentName ?? "-"} clientId=${client3.id ?? "-"} callId=${msg.id}
|
|
107083
107177
|
`);
|
|
107178
|
+
if (!isCronIdentity(client3.agentName))
|
|
107179
|
+
sessionConsumeSignal.noteMainToolCall();
|
|
107084
107180
|
try {
|
|
107085
107181
|
const result = await executeToolCall(msg.tool, msg.args, client3.agentName);
|
|
107086
107182
|
return { type: "tool_call_result", id: msg.id, success: true, result };
|
|
@@ -108769,6 +108865,7 @@ var answerReadyFlush = new AnswerReadyFlushController({
|
|
|
108769
108865
|
turn.answerReadyFlushTimeoutId = handle;
|
|
108770
108866
|
},
|
|
108771
108867
|
onFlush: () => handleSessionEvent2({ kind: "turn_end", durationMs: -1, reason: "answer-ready-quiescence" }),
|
|
108868
|
+
stageWindowMs: resolveAnswerStageMs(process.env),
|
|
108772
108869
|
log: (msg) => process.stderr.write(`telegram gateway: ${msg}
|
|
108773
108870
|
`)
|
|
108774
108871
|
});
|
|
@@ -108907,7 +109004,8 @@ var handbackPreturnSignal = createHandbackPreturnSignal({
|
|
|
108907
109004
|
return false;
|
|
108908
109005
|
return statusKey(live.sessionChatId, live.sessionThreadId) === key;
|
|
108909
109006
|
},
|
|
108910
|
-
isClaudeBusy: () => isMachineInTurn()
|
|
109007
|
+
isClaudeBusy: () => isMachineInTurn(),
|
|
109008
|
+
sessionActivitySince: (sinceTs) => sessionConsumeSignal.activitySince(sinceTs)
|
|
108911
109009
|
});
|
|
108912
109010
|
var memoryLegibilityStager = new MemoryLegibilityStager;
|
|
108913
109011
|
function sendMemoryLegibilityLine(event, chatId, threadId) {
|
|
@@ -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
|
|
13551
|
-
* (answer-ready-flush.ts) — the gateway only
|
|
13552
|
-
* calls `.reset()` / `.clear(turn)`.
|
|
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
|
|
13559
|
-
*
|
|
13560
|
-
*
|
|
13561
|
-
*
|
|
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.
|
|
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
|
|
13571
|
-
*
|
|
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
|
-
//
|
|
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
|
/**
|
|
@@ -238,6 +238,18 @@ export interface HandbackPreturnSignalDeps {
|
|
|
238
238
|
* is the GLOBAL busy signal because the handback queues behind whatever turn
|
|
239
239
|
* is running, regardless of topic. Optional; defaults to "not busy". */
|
|
240
240
|
isClaudeBusy?: () => boolean
|
|
241
|
+
/** True IFF the gateway observed the SESSION consume input after `sinceTs` —
|
|
242
|
+
* a turn minting at the enqueue seam, or a main-session MCP tool call over
|
|
243
|
+
* the IPC bridge (`session-consume-signal.ts`). Consulted at the moment the
|
|
244
|
+
* reap would otherwise declare a genuine orphan: claude holds ONE FIFO input
|
|
245
|
+
* queue, so post-delivery consumption evidence means the delivered handback
|
|
246
|
+
* entered the model's context even though no adopting turn was ever minted
|
|
247
|
+
* (the 2026-08-02 invisible-consumption incident — the session answered the
|
|
248
|
+
* handback via a `reply` from idle with zero session-tail events, then the
|
|
249
|
+
* reap re-injected the already-answered report twice). When true the reap
|
|
250
|
+
* CLEANS UP (card + typing + entry) without re-injecting and without
|
|
251
|
+
* escalating. Optional; defaults to "no evidence" (legacy re-inject). */
|
|
252
|
+
sessionActivitySince?: (sinceTs: number) => boolean
|
|
241
253
|
now?: () => number
|
|
242
254
|
/** Debounce before painting the pre-turn card (kills sub-second flicker). */
|
|
243
255
|
debounceMs?: number
|
|
@@ -487,13 +499,27 @@ export function createHandbackPreturnSignal(
|
|
|
487
499
|
entry.reapTimer = setTimer(() => reap(entry), adoptTimeoutMs)
|
|
488
500
|
return
|
|
489
501
|
}
|
|
502
|
+
// INVISIBLE-CONSUMPTION GATE (2026-08-02 klanker incident). We are at the
|
|
503
|
+
// point the reap would declare a genuine orphan: claude is idle, the
|
|
504
|
+
// delivered TTL elapsed, and no adopting turn ever minted. But "no adopting
|
|
505
|
+
// turn" is NOT proof the handback went un-consumed — the session can
|
|
506
|
+
// consume injected input WITHOUT the gateway minting a turn (zero
|
|
507
|
+
// session-tail events; the only trace was a `reply` tool call from idle).
|
|
508
|
+
// If the gateway observed the session consume input after this handback
|
|
509
|
+
// was delivered (a later turn minting, or a main-session tool call —
|
|
510
|
+
// claude's single FIFO queue means either proves the earlier delivery
|
|
511
|
+
// entered the model's context), the handback was already processed:
|
|
512
|
+
// re-injecting it re-runs an answered report (observed: the same worker
|
|
513
|
+
// report processed three times). Clean up WITHOUT re-injection.
|
|
514
|
+
const consumedElsewhere = deps.sessionActivitySince?.(entry.deliveredAt) === true
|
|
490
515
|
entry.consumed = true
|
|
491
|
-
// GENUINE ORPHAN. Recover DETERMINISTICALLY
|
|
492
|
-
// nudge. Stop the forever-running typing loop,
|
|
493
|
-
// not leave a stale "needs a nudge" message),
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
|
|
516
|
+
// GENUINE ORPHAN (or consumed-elsewhere cleanup). Recover DETERMINISTICALLY
|
|
517
|
+
// — never ask the operator to nudge. Stop the forever-running typing loop,
|
|
518
|
+
// DELETE the frozen card (do not leave a stale "needs a nudge" message),
|
|
519
|
+
// then — for a genuine orphan only — re-inject the handback so the machine
|
|
520
|
+
// re-processes it itself. Cap the re-injections; past the cap escalate to
|
|
521
|
+
// fleet-health telemetry (NOT a chat card).
|
|
522
|
+
stopTypingUnlessTurnLive(entry, consumedElsewhere ? 'consumed reap' : 'orphan reap')
|
|
497
523
|
if (entry.activityMessageId != null) {
|
|
498
524
|
const record: PreTurnCardRecord = {
|
|
499
525
|
turnKey: entry.syntheticTurnKey,
|
|
@@ -518,6 +544,18 @@ export function createHandbackPreturnSignal(
|
|
|
518
544
|
// synchronously) is not swallowed by the per-topic dedupe guard. The entry
|
|
519
545
|
// object stays valid for the captured re-inject below.
|
|
520
546
|
dropEntry(entry)
|
|
547
|
+
if (consumedElsewhere) {
|
|
548
|
+
// Distinct structured line so fleet-health can track how often the
|
|
549
|
+
// session consumes a handback without a gateway-visible adopting turn
|
|
550
|
+
// (the underlying session-tail anomaly stays observable even though the
|
|
551
|
+
// false re-injection is gone).
|
|
552
|
+
log(
|
|
553
|
+
`handback-preturn-signal: reap resolved key=${entry.statusKey} ` +
|
|
554
|
+
`turnId=${entry.adoptTurnId} — session consumed input after delivery ` +
|
|
555
|
+
`(no adopting turn observed); cleaned up without re-injection\n`,
|
|
556
|
+
)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
521
559
|
if (entry.reinjectCount < maxReinjects) {
|
|
522
560
|
const nextCount = entry.reinjectCount + 1
|
|
523
561
|
// Stamp the counter on the inbound so it survives the buffer round trip
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-consume-signal.ts — "did the claude session consume input after T?"
|
|
3
|
+
*
|
|
4
|
+
* Built for the 2026-08-02 klanker false-orphan incident (handback turnId
|
|
5
|
+
* `#1785628912223`). A background worker's `subagent_handback` was released and
|
|
6
|
+
* DELIVERED to the bridge at 00:01:53Z; the session consumed it and answered it
|
|
7
|
+
* (a `reply` tool call at 00:02:58Z, IPC-observed, while the gateway's shadow
|
|
8
|
+
* state was `bridge_alive_idle`) — but NO gateway turn ever minted for it: the
|
|
9
|
+
* session-tail produced no `enqueue`/stream events for that consumption, so
|
|
10
|
+
* `handback-preturn-signal`'s entry was never adopted. After the delivered TTL
|
|
11
|
+
* the reap declared a genuine orphan and RE-INJECTED the already-answered
|
|
12
|
+
* handback — twice — re-processing the same worker report three times.
|
|
13
|
+
*
|
|
14
|
+
* The orphan reap's existing gates (`isClaudeBusy`, the delivered TTL) key off
|
|
15
|
+
* gateway-minted TURN state, which is exactly the signal that was absent. This
|
|
16
|
+
* module tracks the two consumption proxies the gateway CAN always observe,
|
|
17
|
+
* independent of the session-tail:
|
|
18
|
+
*
|
|
19
|
+
* - a TURN MINT (the `enqueue` seam in stream-render): claude holds a single
|
|
20
|
+
* FIFO input queue, so a turn minting for an inbound delivered AFTER the
|
|
21
|
+
* handback proves everything delivered before it entered the model's
|
|
22
|
+
* context (2026-08-02: real turn `#25821` minted 00:04:13Z and completed,
|
|
23
|
+
* while the reap still declared the 00:01:53Z handback orphaned);
|
|
24
|
+
* - a MAIN-SESSION MCP TOOL CALL over the IPC bridge (reply, progress_update,
|
|
25
|
+
* …): the session can only produce a tool call by processing input, so a
|
|
26
|
+
* tool call arriving after the delivery is direct evidence the session is
|
|
27
|
+
* alive and consuming — the 00:02:58Z reply was the ONLY observable trace
|
|
28
|
+
* of the invisible consumption. Cron/foreign sessions are excluded at the
|
|
29
|
+
* call site (their activity says nothing about the main session's queue).
|
|
30
|
+
*
|
|
31
|
+
* Consumers ask `activitySince(deliveredAt)`. The deliberate trade: when this
|
|
32
|
+
* returns true for a handback that was in fact DROPPED before reaching claude
|
|
33
|
+
* (bridge death in the release→write window) while unrelated session activity
|
|
34
|
+
* happened to occur, the reap now cleans up instead of re-injecting — a lost
|
|
35
|
+
* recovery for a rare double-fault, logged distinctly for fleet-health. The
|
|
36
|
+
* previous behaviour re-injected an already-answered handback on every
|
|
37
|
+
* invisible consumption, which is the incident that actually occurs.
|
|
38
|
+
*
|
|
39
|
+
* Pure state, no I/O; module singleton mirrors `subagentReplyAuthority`.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export class SessionConsumeSignal {
|
|
43
|
+
private lastTurnMintAt = 0
|
|
44
|
+
private lastMainToolCallAt = 0
|
|
45
|
+
|
|
46
|
+
/** A gateway turn minted at the `enqueue` seam (stream-render). */
|
|
47
|
+
noteTurnMint(now: number = Date.now()): void {
|
|
48
|
+
if (now > this.lastTurnMintAt) this.lastTurnMintAt = now
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A NON-CRON client's MCP tool call arrived over the IPC bridge. The call
|
|
52
|
+
* site excludes cron identities — a Tier-1 cheap-cron session consuming its
|
|
53
|
+
* own input proves nothing about the main session's queue. */
|
|
54
|
+
noteMainToolCall(now: number = Date.now()): void {
|
|
55
|
+
if (now > this.lastMainToolCallAt) this.lastMainToolCallAt = now
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True IFF either consumption proxy was observed strictly after `sinceTs`. */
|
|
59
|
+
activitySince(sinceTs: number): boolean {
|
|
60
|
+
return this.lastTurnMintAt > sinceTs || this.lastMainToolCallAt > sinceTs
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Test-only: forget all observations. */
|
|
64
|
+
reset(): void {
|
|
65
|
+
this.lastTurnMintAt = 0
|
|
66
|
+
this.lastMainToolCallAt = 0
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The ONE live instance (one CLI session per gateway process — the same
|
|
71
|
+
* module-singleton shape as `subagentReplyAuthority`). */
|
|
72
|
+
export const sessionConsumeSignal = new SessionConsumeSignal()
|
|
@@ -75,6 +75,7 @@ import { isTelegramReplyTool, isTelegramSurfaceTool } from '../tool-names.js'
|
|
|
75
75
|
import { decideTurnFlush } from '../turn-flush-safety.js'
|
|
76
76
|
import { FlushCompletionTracker } from '../flushed-turn-supersede.js'
|
|
77
77
|
import { subagentReplyAuthority } from './subagent-reply-authority.js'
|
|
78
|
+
import { sessionConsumeSignal } from './session-consume-signal.js'
|
|
78
79
|
import { decideTerminalReason, deriveTurnRole } from '../turn-liveness-floor.js'
|
|
79
80
|
import { chatKey, chatKeyWithSuffix } from './chat-key.js'
|
|
80
81
|
import { deriveTurnId } from './derive-turn-id.js'
|
|
@@ -747,6 +748,10 @@ function beginTurn(deps: StreamRenderDeps, ev: TurnStartEnvelope): void {
|
|
|
747
748
|
process.stderr.write(
|
|
748
749
|
`telegram gateway: ${formatTurnLifecycle('set', 'enqueue', next, startedAt)}\n`,
|
|
749
750
|
)
|
|
751
|
+
// Consumption proxy for the handback orphan reap (session-consume-signal):
|
|
752
|
+
// claude holds ONE FIFO input queue, so this turn minting proves every
|
|
753
|
+
// inbound delivered before its own entered the model's context.
|
|
754
|
+
sessionConsumeSignal.noteTurnMint(startedAt)
|
|
750
755
|
// Component 3 — retain in the bounded recently-ended registry so a
|
|
751
756
|
// LATE reply (landing after currentTurn flips to a successor) can
|
|
752
757
|
// still resolve THIS turn's origin thread by its turnId.
|