switchroom 0.14.39 → 0.14.41
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/auth-broker/index.js +294 -46
- package/dist/cli/drive-write-pretool.mjs +25 -1
- package/dist/cli/switchroom.js +63 -6
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +142 -12
- package/telegram-plugin/gateway/gateway.ts +142 -4
- package/telegram-plugin/gateway/inbound-delivery-confirm.ts +160 -0
- package/telegram-plugin/tests/inbound-delivery-confirm.test.ts +180 -0
- package/telegram-plugin/uat/scenarios/inbound-no-drop-rapid-fire-dm.test.ts +64 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +8 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -25257,7 +25257,7 @@ function decodeResponse2(line) {
|
|
|
25257
25257
|
}
|
|
25258
25258
|
return ResponseSchema2.parse(parsed);
|
|
25259
25259
|
}
|
|
25260
|
-
var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
|
|
25260
|
+
var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
|
|
25261
25261
|
var init_protocol2 = __esm(() => {
|
|
25262
25262
|
init_zod();
|
|
25263
25263
|
MAX_FRAME_BYTES2 = 64 * 1024;
|
|
@@ -25361,6 +25361,11 @@ var init_protocol2 = __esm(() => {
|
|
|
25361
25361
|
op: exports_external.literal("list-google-accounts"),
|
|
25362
25362
|
id: exports_external.string().min(1)
|
|
25363
25363
|
});
|
|
25364
|
+
ListMicrosoftAccountsRequestSchema = exports_external.object({
|
|
25365
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
25366
|
+
op: exports_external.literal("list-microsoft-accounts"),
|
|
25367
|
+
id: exports_external.string().min(1)
|
|
25368
|
+
});
|
|
25364
25369
|
ProbeQuotaRequestSchema = exports_external.object({
|
|
25365
25370
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
25366
25371
|
op: exports_external.literal("probe-quota"),
|
|
@@ -25378,6 +25383,7 @@ var init_protocol2 = __esm(() => {
|
|
|
25378
25383
|
RmAccountRequestSchema,
|
|
25379
25384
|
SetOverrideRequestSchema,
|
|
25380
25385
|
ListGoogleAccountsRequestSchema,
|
|
25386
|
+
ListMicrosoftAccountsRequestSchema,
|
|
25381
25387
|
ProbeQuotaRequestSchema
|
|
25382
25388
|
]);
|
|
25383
25389
|
GetCredentialsDataSchema = exports_external.object({
|
|
@@ -25442,6 +25448,16 @@ var init_protocol2 = __esm(() => {
|
|
|
25442
25448
|
ListGoogleAccountsDataSchema = exports_external.object({
|
|
25443
25449
|
accounts: exports_external.array(GoogleAccountStateSchema)
|
|
25444
25450
|
});
|
|
25451
|
+
MicrosoftAccountStateSchema = exports_external.object({
|
|
25452
|
+
account: exports_external.string(),
|
|
25453
|
+
expiresAt: exports_external.number(),
|
|
25454
|
+
scope: exports_external.string(),
|
|
25455
|
+
clientId: exports_external.string(),
|
|
25456
|
+
accountType: exports_external.enum(["personal", "work"])
|
|
25457
|
+
});
|
|
25458
|
+
ListMicrosoftAccountsDataSchema = exports_external.object({
|
|
25459
|
+
accounts: exports_external.array(MicrosoftAccountStateSchema)
|
|
25460
|
+
});
|
|
25445
25461
|
ErrorBodySchema = exports_external.object({
|
|
25446
25462
|
code: exports_external.enum([
|
|
25447
25463
|
"FORBIDDEN",
|
|
@@ -25565,6 +25581,14 @@ class AuthBrokerClient {
|
|
|
25565
25581
|
});
|
|
25566
25582
|
return data;
|
|
25567
25583
|
}
|
|
25584
|
+
async listMicrosoftAccounts() {
|
|
25585
|
+
const data = await this.send({
|
|
25586
|
+
v: PROTOCOL_VERSION,
|
|
25587
|
+
id: randomUUID(),
|
|
25588
|
+
op: "list-microsoft-accounts"
|
|
25589
|
+
});
|
|
25590
|
+
return data;
|
|
25591
|
+
}
|
|
25568
25592
|
async probeQuota(accounts, timeoutMs) {
|
|
25569
25593
|
const data = await this.send({
|
|
25570
25594
|
v: PROTOCOL_VERSION,
|
|
@@ -49438,8 +49462,8 @@ var {
|
|
|
49438
49462
|
} = import__.default;
|
|
49439
49463
|
|
|
49440
49464
|
// src/build-info.ts
|
|
49441
|
-
var VERSION = "0.14.
|
|
49442
|
-
var COMMIT_SHA = "
|
|
49465
|
+
var VERSION = "0.14.41";
|
|
49466
|
+
var COMMIT_SHA = "747ab2f1";
|
|
49443
49467
|
|
|
49444
49468
|
// src/cli/agent.ts
|
|
49445
49469
|
init_source();
|
|
@@ -57992,13 +58016,46 @@ function registerAccountRemove2(accountParent) {
|
|
|
57992
58016
|
}));
|
|
57993
58017
|
}
|
|
57994
58018
|
function registerAccountList2(accountParent) {
|
|
57995
|
-
accountParent.command("list").description("List Microsoft accounts the broker holds credentials for. Distinct from `auth microsoft list` (YAML ACL matrix).").option("--json", "Emit raw JSON").action(withConfigError(async (
|
|
58019
|
+
accountParent.command("list").description("List Microsoft accounts the broker holds credentials for. Distinct from `auth microsoft list` (YAML ACL matrix).").option("--json", "Emit raw JSON").action(withConfigError(async (opts) => {
|
|
58020
|
+
const { brokerCall: brokerCall2 } = await Promise.resolve().then(() => (init_broker_call(), exports_broker_call));
|
|
58021
|
+
const data = await brokerCall2(async (client) => client.listMicrosoftAccounts());
|
|
58022
|
+
if (opts.json) {
|
|
58023
|
+
console.log(JSON.stringify(data, null, 2));
|
|
58024
|
+
return;
|
|
58025
|
+
}
|
|
57996
58026
|
console.log();
|
|
57997
|
-
|
|
57998
|
-
|
|
58027
|
+
if (data.accounts.length === 0) {
|
|
58028
|
+
console.log(source_default.gray(" No Microsoft accounts stored in broker."));
|
|
58029
|
+
console.log(` Add one: ${source_default.bold("switchroom auth microsoft account add <email>")}`);
|
|
58030
|
+
console.log();
|
|
58031
|
+
return;
|
|
58032
|
+
}
|
|
58033
|
+
const accountColWidth = Math.max(...data.accounts.map((a) => a.account.length), "ACCOUNT".length);
|
|
58034
|
+
const typeColWidth = "TYPE".length + 4;
|
|
58035
|
+
const expiresColWidth = "EXPIRES".length + 2;
|
|
58036
|
+
console.log(`${pad2("ACCOUNT", accountColWidth)} ${pad2("TYPE", typeColWidth)} ${pad2("EXPIRES", expiresColWidth)} SCOPE`);
|
|
58037
|
+
console.log(`${pad2("-".repeat(7), accountColWidth)} ${pad2("-".repeat(4), typeColWidth)} ${pad2("-".repeat(7), expiresColWidth)} ${"-".repeat(5)}`);
|
|
58038
|
+
const now = Date.now();
|
|
58039
|
+
for (const a of data.accounts) {
|
|
58040
|
+
const expiresLabel = formatMicrosoftExpiry(a.expiresAt - now);
|
|
58041
|
+
const scopes = a.scope.split(" ").filter((s) => s.length > 0 && !["openid", "profile", "email", "offline_access"].includes(s)).join(", ");
|
|
58042
|
+
console.log(`${pad2(a.account, accountColWidth)} ${pad2(a.accountType, typeColWidth)} ${pad2(expiresLabel, expiresColWidth)} ${scopes}`);
|
|
58043
|
+
}
|
|
57999
58044
|
console.log();
|
|
58000
58045
|
}));
|
|
58001
58046
|
}
|
|
58047
|
+
function formatMicrosoftExpiry(remainingMs) {
|
|
58048
|
+
if (remainingMs <= 0)
|
|
58049
|
+
return source_default.red("expired");
|
|
58050
|
+
const minutes = Math.round(remainingMs / 60000);
|
|
58051
|
+
if (minutes < 60)
|
|
58052
|
+
return `${minutes}m`;
|
|
58053
|
+
const hours = Math.round(minutes / 60);
|
|
58054
|
+
if (hours < 48)
|
|
58055
|
+
return `${hours}h`;
|
|
58056
|
+
const days = Math.round(hours / 24);
|
|
58057
|
+
return `${days}d`;
|
|
58058
|
+
}
|
|
58002
58059
|
function validateAndNormalizeAccountEmail2(account) {
|
|
58003
58060
|
const normalized = account.trim().toLowerCase();
|
|
58004
58061
|
if (!/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/.test(normalized)) {
|
package/package.json
CHANGED
|
@@ -16160,7 +16160,7 @@ function decodeResponse(line) {
|
|
|
16160
16160
|
}
|
|
16161
16161
|
return ResponseSchema.parse(parsed);
|
|
16162
16162
|
}
|
|
16163
|
-
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
16163
|
+
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
16164
16164
|
var init_protocol = __esm(() => {
|
|
16165
16165
|
init_zod();
|
|
16166
16166
|
MAX_FRAME_BYTES = 64 * 1024;
|
|
@@ -16264,6 +16264,11 @@ var init_protocol = __esm(() => {
|
|
|
16264
16264
|
op: exports_external.literal("list-google-accounts"),
|
|
16265
16265
|
id: exports_external.string().min(1)
|
|
16266
16266
|
});
|
|
16267
|
+
ListMicrosoftAccountsRequestSchema = exports_external.object({
|
|
16268
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
16269
|
+
op: exports_external.literal("list-microsoft-accounts"),
|
|
16270
|
+
id: exports_external.string().min(1)
|
|
16271
|
+
});
|
|
16267
16272
|
ProbeQuotaRequestSchema = exports_external.object({
|
|
16268
16273
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
16269
16274
|
op: exports_external.literal("probe-quota"),
|
|
@@ -16281,6 +16286,7 @@ var init_protocol = __esm(() => {
|
|
|
16281
16286
|
RmAccountRequestSchema,
|
|
16282
16287
|
SetOverrideRequestSchema,
|
|
16283
16288
|
ListGoogleAccountsRequestSchema,
|
|
16289
|
+
ListMicrosoftAccountsRequestSchema,
|
|
16284
16290
|
ProbeQuotaRequestSchema
|
|
16285
16291
|
]);
|
|
16286
16292
|
GetCredentialsDataSchema = exports_external.object({
|
|
@@ -16345,6 +16351,16 @@ var init_protocol = __esm(() => {
|
|
|
16345
16351
|
ListGoogleAccountsDataSchema = exports_external.object({
|
|
16346
16352
|
accounts: exports_external.array(GoogleAccountStateSchema)
|
|
16347
16353
|
});
|
|
16354
|
+
MicrosoftAccountStateSchema = exports_external.object({
|
|
16355
|
+
account: exports_external.string(),
|
|
16356
|
+
expiresAt: exports_external.number(),
|
|
16357
|
+
scope: exports_external.string(),
|
|
16358
|
+
clientId: exports_external.string(),
|
|
16359
|
+
accountType: exports_external.enum(["personal", "work"])
|
|
16360
|
+
});
|
|
16361
|
+
ListMicrosoftAccountsDataSchema = exports_external.object({
|
|
16362
|
+
accounts: exports_external.array(MicrosoftAccountStateSchema)
|
|
16363
|
+
});
|
|
16348
16364
|
ErrorBodySchema = exports_external.object({
|
|
16349
16365
|
code: exports_external.enum([
|
|
16350
16366
|
"FORBIDDEN",
|
|
@@ -16468,6 +16484,14 @@ class AuthBrokerClient {
|
|
|
16468
16484
|
});
|
|
16469
16485
|
return data;
|
|
16470
16486
|
}
|
|
16487
|
+
async listMicrosoftAccounts() {
|
|
16488
|
+
const data = await this.send({
|
|
16489
|
+
v: PROTOCOL_VERSION,
|
|
16490
|
+
id: randomUUID3(),
|
|
16491
|
+
op: "list-microsoft-accounts"
|
|
16492
|
+
});
|
|
16493
|
+
return data;
|
|
16494
|
+
}
|
|
16471
16495
|
async probeQuota(accounts, timeoutMs) {
|
|
16472
16496
|
const data = await this.send({
|
|
16473
16497
|
v: PROTOCOL_VERSION,
|
|
@@ -40682,6 +40706,14 @@ class AuthBrokerClient2 {
|
|
|
40682
40706
|
});
|
|
40683
40707
|
return data;
|
|
40684
40708
|
}
|
|
40709
|
+
async listMicrosoftAccounts() {
|
|
40710
|
+
const data = await this.send({
|
|
40711
|
+
v: PROTOCOL_VERSION,
|
|
40712
|
+
id: randomUUID4(),
|
|
40713
|
+
op: "list-microsoft-accounts"
|
|
40714
|
+
});
|
|
40715
|
+
return data;
|
|
40716
|
+
}
|
|
40685
40717
|
async probeQuota(accounts, timeoutMs) {
|
|
40686
40718
|
const data = await this.send({
|
|
40687
40719
|
v: PROTOCOL_VERSION,
|
|
@@ -47227,6 +47259,45 @@ function decideInboundDelivery(input) {
|
|
|
47227
47259
|
return "deliver";
|
|
47228
47260
|
}
|
|
47229
47261
|
|
|
47262
|
+
// gateway/inbound-delivery-confirm.ts
|
|
47263
|
+
function createDeliveryQueue() {
|
|
47264
|
+
return { pending: new Map };
|
|
47265
|
+
}
|
|
47266
|
+
function trackDelivery(q, key, inbound, now, messageId = null) {
|
|
47267
|
+
q.pending.set(key, { key, inbound, messageId, lastAttemptAt: now });
|
|
47268
|
+
}
|
|
47269
|
+
function ackDelivery(q, key, enqueueMessageId = null) {
|
|
47270
|
+
const entry = q.pending.get(key);
|
|
47271
|
+
if (!entry)
|
|
47272
|
+
return false;
|
|
47273
|
+
if (entry.messageId != null && entry.messageId !== enqueueMessageId)
|
|
47274
|
+
return false;
|
|
47275
|
+
q.pending.delete(key);
|
|
47276
|
+
return true;
|
|
47277
|
+
}
|
|
47278
|
+
function sweep(q, now, timeoutMs) {
|
|
47279
|
+
const redeliver = [];
|
|
47280
|
+
for (const entry of q.pending.values()) {
|
|
47281
|
+
if (now - entry.lastAttemptAt < timeoutMs)
|
|
47282
|
+
continue;
|
|
47283
|
+
entry.lastAttemptAt = now;
|
|
47284
|
+
redeliver.push(entry);
|
|
47285
|
+
}
|
|
47286
|
+
return redeliver;
|
|
47287
|
+
}
|
|
47288
|
+
function forgetDelivery(q, key) {
|
|
47289
|
+
q.pending.delete(key);
|
|
47290
|
+
}
|
|
47291
|
+
function shouldTrackDelivery(input) {
|
|
47292
|
+
if (input.isSteering || input.isInterrupt)
|
|
47293
|
+
return false;
|
|
47294
|
+
if (input.hasSource)
|
|
47295
|
+
return false;
|
|
47296
|
+
if (input.effectiveText !== undefined && input.effectiveText.trim().length === 0)
|
|
47297
|
+
return false;
|
|
47298
|
+
return true;
|
|
47299
|
+
}
|
|
47300
|
+
|
|
47230
47301
|
// gateway/pending-permission-decisions.ts
|
|
47231
47302
|
var DEFAULT_PENDING_PERMISSION_CAP = 32;
|
|
47232
47303
|
function createPendingPermissionBuffer(opts = {}) {
|
|
@@ -47411,12 +47482,12 @@ function transition(state3, event) {
|
|
|
47411
47482
|
turnStartedAt: null,
|
|
47412
47483
|
lastOutboundAt: event.outboundEmitted ? event.at : p.lastOutboundAt
|
|
47413
47484
|
}));
|
|
47414
|
-
const
|
|
47485
|
+
const sweep2 = sweepSiblings(stateAfterClear, chatId, event.key);
|
|
47415
47486
|
const wasActive = state3.global.kind === "bridge_alive_in_turn" && state3.global.activeTurn === event.key;
|
|
47416
|
-
const next = wasActive ? { ...
|
|
47487
|
+
const next = wasActive ? { ...sweep2.state, global: { kind: "bridge_alive_idle" } } : sweep2.state;
|
|
47417
47488
|
const effects = [
|
|
47418
47489
|
{ kind: "clearTurnStarted", key: event.key },
|
|
47419
|
-
...
|
|
47490
|
+
...sweep2.effects
|
|
47420
47491
|
];
|
|
47421
47492
|
if (event.outboundEmitted) {
|
|
47422
47493
|
effects.push({ kind: "noteOutbound", key: event.key, at: event.at });
|
|
@@ -51794,10 +51865,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
51794
51865
|
}
|
|
51795
51866
|
|
|
51796
51867
|
// ../src/build-info.ts
|
|
51797
|
-
var VERSION = "0.14.
|
|
51798
|
-
var COMMIT_SHA = "
|
|
51799
|
-
var COMMIT_DATE = "2026-06-
|
|
51800
|
-
var LATEST_PR =
|
|
51868
|
+
var VERSION = "0.14.41";
|
|
51869
|
+
var COMMIT_SHA = "747ab2f1";
|
|
51870
|
+
var COMMIT_DATE = "2026-06-02T11:03:39Z";
|
|
51871
|
+
var LATEST_PR = 2095;
|
|
51801
51872
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
51802
51873
|
|
|
51803
51874
|
// gateway/boot-version.ts
|
|
@@ -52967,8 +53038,16 @@ function markClaudeBusyForInbound(m) {
|
|
|
52967
53038
|
if (Number.isFinite(n))
|
|
52968
53039
|
tid = n;
|
|
52969
53040
|
}
|
|
52970
|
-
|
|
53041
|
+
const key = chatKey2(m.chatId, tid);
|
|
53042
|
+
claudeBusyKeys.add(key);
|
|
53043
|
+
return key;
|
|
52971
53044
|
}
|
|
53045
|
+
var DELIVERY_CONFIRM_ENABLED = process.env.SWITCHROOM_INBOUND_DELIVERY_CONFIRM !== "0";
|
|
53046
|
+
var _deliveryTimeoutRaw = process.env.SWITCHROOM_INBOUND_DELIVERY_TIMEOUT_MS;
|
|
53047
|
+
var _deliveryTimeoutParsed = _deliveryTimeoutRaw != null && _deliveryTimeoutRaw !== "" ? Number(_deliveryTimeoutRaw) : 15000;
|
|
53048
|
+
var DELIVERY_CONFIRM_TIMEOUT_MS = Number.isFinite(_deliveryTimeoutParsed) && _deliveryTimeoutParsed > 0 ? _deliveryTimeoutParsed : 15000;
|
|
53049
|
+
var DELIVERY_CONFIRM_SWEEP_MS = 5000;
|
|
53050
|
+
var deliveryQueue = createDeliveryQueue();
|
|
52972
53051
|
function turnInFlightForGate() {
|
|
52973
53052
|
return isDeliveryCutoverEnabled() ? isMachineInTurn() : claudeBusyKeys.size > 0;
|
|
52974
53053
|
}
|
|
@@ -54088,6 +54167,38 @@ var _deliveryMachineTick = setInterval(() => {
|
|
|
54088
54167
|
shadowEmit({ kind: "tick", now: Date.now() });
|
|
54089
54168
|
}, DELIVERY_MACHINE_TICK_MS);
|
|
54090
54169
|
_deliveryMachineTick.unref?.();
|
|
54170
|
+
async function redeliverStrandedInbound(p) {
|
|
54171
|
+
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
54172
|
+
process.stderr.write(`telegram gateway: inbound strand (no enqueue ack) key=${p.key} \u2014 re-clearing composer + re-delivering
|
|
54173
|
+
`);
|
|
54174
|
+
try {
|
|
54175
|
+
const { clearAgentComposer: clearAgentComposer2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
54176
|
+
if (selfAgent)
|
|
54177
|
+
clearAgentComposer2({ agentName: selfAgent });
|
|
54178
|
+
} catch {}
|
|
54179
|
+
const ok = ipcServer.sendToAgent(selfAgent, p.inbound);
|
|
54180
|
+
if (ok) {
|
|
54181
|
+
markClaudeBusyForInbound(p.inbound);
|
|
54182
|
+
if (!deliveryQueue.pending.has(p.key)) {
|
|
54183
|
+
trackDelivery(deliveryQueue, p.key, p.inbound, Date.now(), p.messageId);
|
|
54184
|
+
}
|
|
54185
|
+
} else {
|
|
54186
|
+
pendingInboundBuffer.push(selfAgent, p.inbound);
|
|
54187
|
+
forgetDelivery(deliveryQueue, p.key);
|
|
54188
|
+
}
|
|
54189
|
+
}
|
|
54190
|
+
var _deliveryConfirmSweep = setInterval(() => {
|
|
54191
|
+
if (!DELIVERY_CONFIRM_ENABLED)
|
|
54192
|
+
return;
|
|
54193
|
+
if (currentTurn != null)
|
|
54194
|
+
return;
|
|
54195
|
+
if (pendingPermissions.size > 0 || pendingAskUser.size > 0)
|
|
54196
|
+
return;
|
|
54197
|
+
for (const p of sweep(deliveryQueue, Date.now(), DELIVERY_CONFIRM_TIMEOUT_MS)) {
|
|
54198
|
+
redeliverStrandedInbound(p);
|
|
54199
|
+
}
|
|
54200
|
+
}, DELIVERY_CONFIRM_SWEEP_MS);
|
|
54201
|
+
_deliveryConfirmSweep.unref?.();
|
|
54091
54202
|
startTimer2({
|
|
54092
54203
|
editMessage: async (ctx) => {
|
|
54093
54204
|
const editOpts = ctx.parseMode != null ? { parse_mode: ctx.parseMode } : undefined;
|
|
@@ -56478,6 +56589,9 @@ function handleSessionEvent(ev) {
|
|
|
56478
56589
|
isDm: isDmChatId(ev.chatId)
|
|
56479
56590
|
};
|
|
56480
56591
|
currentTurn = next;
|
|
56592
|
+
if (DELIVERY_CONFIRM_ENABLED) {
|
|
56593
|
+
ackDelivery(deliveryQueue, chatKey2(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null), ev.messageId);
|
|
56594
|
+
}
|
|
56481
56595
|
shadowEmit({
|
|
56482
56596
|
kind: "turnStart",
|
|
56483
56597
|
key: statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : undefined),
|
|
@@ -57910,10 +58024,26 @@ ${preBlock(write.output)}`;
|
|
|
57910
58024
|
}
|
|
57911
58025
|
}
|
|
57912
58026
|
const delivered = ipcServer.sendToAgent(selfAgent, inboundMsg);
|
|
57913
|
-
if (delivered)
|
|
57914
|
-
markClaudeBusyForInbound(inboundMsg);
|
|
58027
|
+
if (delivered) {
|
|
58028
|
+
const busyKey = markClaudeBusyForInbound(inboundMsg);
|
|
58029
|
+
if (DELIVERY_CONFIRM_ENABLED && shouldTrackDelivery({
|
|
58030
|
+
isSteering,
|
|
58031
|
+
isInterrupt: interrupt.isInterrupt,
|
|
58032
|
+
hasSource: inboundMsg.meta?.source != null,
|
|
58033
|
+
effectiveText
|
|
58034
|
+
})) {
|
|
58035
|
+
trackDelivery(deliveryQueue, busyKey, inboundMsg, Date.now(), String(inboundMsg.messageId));
|
|
58036
|
+
}
|
|
58037
|
+
}
|
|
57915
58038
|
if (!delivered) {
|
|
57916
|
-
|
|
58039
|
+
if (shouldTrackDelivery({
|
|
58040
|
+
isSteering,
|
|
58041
|
+
isInterrupt: interrupt.isInterrupt,
|
|
58042
|
+
hasSource: inboundMsg.meta?.source != null,
|
|
58043
|
+
effectiveText
|
|
58044
|
+
})) {
|
|
58045
|
+
pendingInboundBuffer.push(selfAgent, inboundMsg);
|
|
58046
|
+
}
|
|
57917
58047
|
const threadOpts = messageThreadId != null ? { message_thread_id: messageThreadId } : {};
|
|
57918
58048
|
swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u23F3 Agent is restarting \u2014 your message is queued and will be processed when it reconnects.", { ...threadOpts }), {
|
|
57919
58049
|
chat_id,
|
|
@@ -280,6 +280,15 @@ import { createPendingInboundBuffer, redeliverBufferedInbound, idleDrainTick } f
|
|
|
280
280
|
import { createInboundSpool } from './inbound-spool.js'
|
|
281
281
|
import { purgeStaleTurnsForChat } from './turn-state-purge.js'
|
|
282
282
|
import { decideInboundDelivery } from './inbound-delivery-gate.js'
|
|
283
|
+
import {
|
|
284
|
+
createDeliveryQueue,
|
|
285
|
+
trackDelivery,
|
|
286
|
+
ackDelivery,
|
|
287
|
+
sweep as sweepDeliveryQueue,
|
|
288
|
+
forgetDelivery,
|
|
289
|
+
shouldTrackDelivery,
|
|
290
|
+
type PendingDelivery,
|
|
291
|
+
} from './inbound-delivery-confirm.js'
|
|
283
292
|
import { createPendingPermissionBuffer } from './pending-permission-decisions.js'
|
|
284
293
|
import { chatKey, chatKeyWithSuffix, chatIdOfChatKey } from './chat-key.js'
|
|
285
294
|
// Phase 2b PR 2 — shadow mode. Each event-site below calls shadowEmit()
|
|
@@ -1309,15 +1318,43 @@ function markClaudeBusyForInbound(m: {
|
|
|
1309
1318
|
chatId: string
|
|
1310
1319
|
threadId?: number
|
|
1311
1320
|
meta?: Record<string, string>
|
|
1312
|
-
}):
|
|
1321
|
+
}): string {
|
|
1313
1322
|
let tid: number | null = m.threadId ?? null
|
|
1314
1323
|
if (tid == null && m.meta?.message_thread_id != null) {
|
|
1315
1324
|
const n = Number(m.meta.message_thread_id)
|
|
1316
1325
|
if (Number.isFinite(n)) tid = n
|
|
1317
1326
|
}
|
|
1318
|
-
|
|
1327
|
+
const key = chatKey(m.chatId, tid)
|
|
1328
|
+
claudeBusyKeys.add(key)
|
|
1329
|
+
return key
|
|
1319
1330
|
}
|
|
1320
1331
|
|
|
1332
|
+
// ─── Reliable inbound delivery: deliver-until-acked (the marko drop-wedge) ─
|
|
1333
|
+
// A delivered inbound is ACKED only by the `enqueue` session-event (claude
|
|
1334
|
+
// actually started the turn) — NOT by sendToAgent returning true. Until
|
|
1335
|
+
// acked, the message stays queued; if it didn't land within the timeout it
|
|
1336
|
+
// stranded in claude's composer, so we re-deliver it. Re-deliver forever
|
|
1337
|
+
// until acked — never drop (vs the old fire-and-forget that the 300s
|
|
1338
|
+
// silence-poke swallowed). Kill switch: SWITCHROOM_INBOUND_DELIVERY_CONFIRM=0
|
|
1339
|
+
// → legacy fire-and-forget. See inbound-delivery-confirm.ts.
|
|
1340
|
+
const DELIVERY_CONFIRM_ENABLED = process.env.SWITCHROOM_INBOUND_DELIVERY_CONFIRM !== '0'
|
|
1341
|
+
// How long to wait for claude's `enqueue` ack before treating a delivery as
|
|
1342
|
+
// stranded and re-delivering. Generous by default — claude acks within ~1s of
|
|
1343
|
+
// a clean delivery, so 15s won't false-positive on a healthy turn. Tunable
|
|
1344
|
+
// (env) for tests/forensics; a too-low value re-delivers healthy slow turns
|
|
1345
|
+
// (duplicate turn), which is why the default is comfortably above ack latency.
|
|
1346
|
+
const _deliveryTimeoutRaw = process.env.SWITCHROOM_INBOUND_DELIVERY_TIMEOUT_MS
|
|
1347
|
+
const _deliveryTimeoutParsed =
|
|
1348
|
+
_deliveryTimeoutRaw != null && _deliveryTimeoutRaw !== '' ? Number(_deliveryTimeoutRaw) : 15_000
|
|
1349
|
+
// Clamp to a positive, finite value: a negative / zero / NaN env override would
|
|
1350
|
+
// make the sweep treat every tracked entry as stranded and re-deliver every
|
|
1351
|
+
// cycle forever (a self-inflicted re-delivery loop). To disable the feature,
|
|
1352
|
+
// use SWITCHROOM_INBOUND_DELIVERY_CONFIRM=0, not a degenerate timeout.
|
|
1353
|
+
const DELIVERY_CONFIRM_TIMEOUT_MS =
|
|
1354
|
+
Number.isFinite(_deliveryTimeoutParsed) && _deliveryTimeoutParsed > 0 ? _deliveryTimeoutParsed : 15_000
|
|
1355
|
+
const DELIVERY_CONFIRM_SWEEP_MS = 5_000
|
|
1356
|
+
const deliveryQueue = createDeliveryQueue<InboundMessage>()
|
|
1357
|
+
|
|
1321
1358
|
/**
|
|
1322
1359
|
* Authoritative "is a turn in flight?" for every gate that previously
|
|
1323
1360
|
* read `claudeBusyKeys.size`. PR 3b cutover (extends PR 3a's bridgeUp
|
|
@@ -4061,6 +4098,57 @@ const _deliveryMachineTick = setInterval(() => {
|
|
|
4061
4098
|
}, DELIVERY_MACHINE_TICK_MS)
|
|
4062
4099
|
_deliveryMachineTick.unref?.()
|
|
4063
4100
|
|
|
4101
|
+
// Re-deliver stranded inbounds until claude acks (the marko drop-wedge).
|
|
4102
|
+
// Every few seconds, re-send any inbound that was handed to claude but never
|
|
4103
|
+
// acked by an `enqueue` — it stranded unsubmitted in the composer. Re-clear
|
|
4104
|
+
// the composer so the re-sent notification lands on a clean line, then
|
|
4105
|
+
// re-send. Reuses the same delivery primitives; the message is never dropped.
|
|
4106
|
+
// (Refs ipcServer / pendingInboundBuffer declared below — resolved at fire
|
|
4107
|
+
// time, after module init.) unref so the interval never holds the process.
|
|
4108
|
+
async function redeliverStrandedInbound(p: PendingDelivery<InboundMessage>): Promise<void> {
|
|
4109
|
+
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? ''
|
|
4110
|
+
process.stderr.write(
|
|
4111
|
+
`telegram gateway: inbound strand (no enqueue ack) key=${p.key} — re-clearing composer + re-delivering\n`,
|
|
4112
|
+
)
|
|
4113
|
+
try {
|
|
4114
|
+
const { clearAgentComposer } = await import('../../src/agents/tmux.js')
|
|
4115
|
+
if (selfAgent) clearAgentComposer({ agentName: selfAgent })
|
|
4116
|
+
} catch { /* best-effort; re-deliver regardless */ }
|
|
4117
|
+
const ok = ipcServer.sendToAgent(selfAgent, p.inbound)
|
|
4118
|
+
if (ok) {
|
|
4119
|
+
// Keep the #1556 gate coherent with the re-sent delivery, and survive an
|
|
4120
|
+
// ack that raced the `await import` above: only `enqueue` clears tracking,
|
|
4121
|
+
// so if a concurrent ack removed the entry, re-affirm it — never drop.
|
|
4122
|
+
// Both ops are idempotent.
|
|
4123
|
+
markClaudeBusyForInbound(p.inbound)
|
|
4124
|
+
if (!deliveryQueue.pending.has(p.key)) {
|
|
4125
|
+
trackDelivery(deliveryQueue, p.key, p.inbound, Date.now(), p.messageId)
|
|
4126
|
+
}
|
|
4127
|
+
} else {
|
|
4128
|
+
// Bridge offline between attempts — hand off to the offline buffer
|
|
4129
|
+
// (bridgeUp drains it) and stop tracking here; the spool owns it now.
|
|
4130
|
+
pendingInboundBuffer.push(selfAgent, p.inbound)
|
|
4131
|
+
forgetDelivery(deliveryQueue, p.key)
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
const _deliveryConfirmSweep = setInterval(() => {
|
|
4135
|
+
if (!DELIVERY_CONFIRM_ENABLED) return
|
|
4136
|
+
// Re-deliver ONLY when claude is genuinely idle. `currentTurn` is set solely
|
|
4137
|
+
// by the enqueue session-event and nulled at turn-end, so `currentTurn != null`
|
|
4138
|
+
// means a real turn is in flight — re-clearing the composer + re-sending now
|
|
4139
|
+
// would clobber it (the exact mid-turn wedge this queue exists to prevent). A
|
|
4140
|
+
// pending permission / ask_user prompt is likewise a live interaction. Defer:
|
|
4141
|
+
// leave the entry pending (it isn't acked) so the next idle sweep retries.
|
|
4142
|
+
// NB: claudeBusyKeys (turnInFlightForGate) is set EAGERLY at delivery and
|
|
4143
|
+
// stays set through a strand, so it is NOT a usable "idle" signal here.
|
|
4144
|
+
if (currentTurn != null) return
|
|
4145
|
+
if (pendingPermissions.size > 0 || pendingAskUser.size > 0) return
|
|
4146
|
+
for (const p of sweepDeliveryQueue(deliveryQueue, Date.now(), DELIVERY_CONFIRM_TIMEOUT_MS)) {
|
|
4147
|
+
void redeliverStrandedInbound(p)
|
|
4148
|
+
}
|
|
4149
|
+
}, DELIVERY_CONFIRM_SWEEP_MS)
|
|
4150
|
+
_deliveryConfirmSweep.unref?.()
|
|
4151
|
+
|
|
4064
4152
|
// #1445 cross-turn pending-async ambient. When a turn ends after the
|
|
4065
4153
|
// model dispatched background async work (Agent / Task / Bash run-in-
|
|
4066
4154
|
// background) and the model has stopped speaking, keep editing the
|
|
@@ -7880,6 +7968,21 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
7880
7968
|
isDm: isDmChatId(ev.chatId),
|
|
7881
7969
|
}
|
|
7882
7970
|
currentTurn = next
|
|
7971
|
+
// Ack inbound delivery (the marko drop-wedge): claude actually started
|
|
7972
|
+
// this turn, so its delivered inbound landed — stop tracking it for
|
|
7973
|
+
// re-delivery. `enqueue` carries the same chat/thread the inbound was
|
|
7974
|
+
// keyed on, so the key matches.
|
|
7975
|
+
if (DELIVERY_CONFIRM_ENABLED) {
|
|
7976
|
+
// Match on the source message id: `enqueue` fires for EVERY turn
|
|
7977
|
+
// start (cron / subagent-handback / vault-resume / restart-marker
|
|
7978
|
+
// too — see comment below), so a key-only ack would let a synthetic
|
|
7979
|
+
// turn clear a real user message still waiting under the same key.
|
|
7980
|
+
ackDelivery(
|
|
7981
|
+
deliveryQueue,
|
|
7982
|
+
chatKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null),
|
|
7983
|
+
ev.messageId,
|
|
7984
|
+
)
|
|
7985
|
+
}
|
|
7883
7986
|
// PR3b-cutover: feed the authoritative turn-start to the delivery
|
|
7884
7987
|
// machine. `enqueue` fires for EVERY turn atom regardless of
|
|
7885
7988
|
// source — inbound, cron, subagent-handback, vault-resume,
|
|
@@ -10596,9 +10699,44 @@ async function handleInbound(
|
|
|
10596
10699
|
}
|
|
10597
10700
|
|
|
10598
10701
|
const delivered = ipcServer.sendToAgent(selfAgent, inboundMsg)
|
|
10599
|
-
if (delivered)
|
|
10702
|
+
if (delivered) {
|
|
10703
|
+
const busyKey = markClaudeBusyForInbound(inboundMsg)
|
|
10704
|
+
// Track until claude acks via `enqueue` (the marko drop-wedge): if no ack
|
|
10705
|
+
// lands, the message stranded in the composer and the sweep re-delivers
|
|
10706
|
+
// it. Track ONLY messages that produce an `enqueue` to ack against —
|
|
10707
|
+
// shouldTrackDelivery excludes steering / `!` interrupt (amend the running
|
|
10708
|
+
// turn), synthetic (meta.source) inbounds, and empty bodies, all of which
|
|
10709
|
+
// never enqueue and would re-deliver forever. The tracked messageId lets
|
|
10710
|
+
// the ack match only THIS message's enqueue (not a synthetic turn sharing
|
|
10711
|
+
// the key). See shouldTrackDelivery / ackDelivery (inbound-delivery-confirm.ts).
|
|
10712
|
+
if (
|
|
10713
|
+
DELIVERY_CONFIRM_ENABLED &&
|
|
10714
|
+
shouldTrackDelivery({
|
|
10715
|
+
isSteering,
|
|
10716
|
+
isInterrupt: interrupt.isInterrupt,
|
|
10717
|
+
hasSource: inboundMsg.meta?.source != null,
|
|
10718
|
+
effectiveText,
|
|
10719
|
+
})
|
|
10720
|
+
) {
|
|
10721
|
+
trackDelivery(deliveryQueue, busyKey, inboundMsg, Date.now(), String(inboundMsg.messageId))
|
|
10722
|
+
}
|
|
10723
|
+
}
|
|
10600
10724
|
if (!delivered) {
|
|
10601
|
-
|
|
10725
|
+
// Only persist fresh user turns to the durable spool. Steering / `!`
|
|
10726
|
+
// interrupt / empty bodies are mid-turn amendments or no-ops that would
|
|
10727
|
+
// arrive orphaned if replayed as a fresh turn after a restart — drop them
|
|
10728
|
+
// (the restart notice below tells the user to re-send). Mirrors the
|
|
10729
|
+
// tracking + #1556-gate carve-outs.
|
|
10730
|
+
if (
|
|
10731
|
+
shouldTrackDelivery({
|
|
10732
|
+
isSteering,
|
|
10733
|
+
isInterrupt: interrupt.isInterrupt,
|
|
10734
|
+
hasSource: inboundMsg.meta?.source != null,
|
|
10735
|
+
effectiveText,
|
|
10736
|
+
})
|
|
10737
|
+
) {
|
|
10738
|
+
pendingInboundBuffer.push(selfAgent, inboundMsg)
|
|
10739
|
+
}
|
|
10602
10740
|
const threadOpts = messageThreadId != null ? { message_thread_id: messageThreadId } : {}
|
|
10603
10741
|
// #1075: thread-id-bearing — swallow via robustApiCall so a deleted
|
|
10604
10742
|
// topic doesn't crash the gateway. Fire-and-forget; the inbound is
|