switchroom 0.13.26 → 0.13.27
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/cli/switchroom.js +2 -2
- package/package.json +1 -1
- package/telegram-plugin/active-reactions-sweep.ts +4 -4
- package/telegram-plugin/dist/gateway/gateway.js +239 -64
- package/telegram-plugin/docs/waiting-ux-spec.md +17 -1
- package/telegram-plugin/gateway/disconnect-flush.ts +10 -6
- package/telegram-plugin/gateway/gateway.ts +166 -51
- package/telegram-plugin/gateway/inbound-spool.ts +69 -2
- package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +14 -0
- package/telegram-plugin/gateway/subagent-progress-inbound-builder.ts +256 -0
- package/telegram-plugin/pending-work-progress.ts +5 -1
- package/telegram-plugin/status-reactions.ts +70 -58
- package/telegram-plugin/stream-reply-handler.ts +7 -36
- package/telegram-plugin/subagent-watcher.ts +64 -3
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -3
- package/telegram-plugin/tests/inbound-spool-progress.test.ts +213 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +62 -0
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +0 -1
- package/telegram-plugin/tests/outbound-ordering.test.ts +0 -1
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -1
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +78 -135
- package/telegram-plugin/tests/status-accent.test.ts +0 -1
- package/telegram-plugin/tests/status-reactions.test.ts +56 -27
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +0 -1
- package/telegram-plugin/tests/stream-reply-handler.test.ts +9 -25
- package/telegram-plugin/tests/streaming-e2e.test.ts +0 -1
- package/telegram-plugin/tests/streaming-orchestration.test.ts +0 -1
- package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +22 -0
- package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +269 -0
- package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +204 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -47436,8 +47436,8 @@ var {
|
|
|
47436
47436
|
} = import__.default;
|
|
47437
47437
|
|
|
47438
47438
|
// src/build-info.ts
|
|
47439
|
-
var VERSION = "0.13.
|
|
47440
|
-
var COMMIT_SHA = "
|
|
47439
|
+
var VERSION = "0.13.27";
|
|
47440
|
+
var COMMIT_SHA = "a158e029";
|
|
47441
47441
|
|
|
47442
47442
|
// src/cli/agent.ts
|
|
47443
47443
|
init_source();
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* any still-active reactions to 👍 before it gets SIGTERM'd.
|
|
17
17
|
*
|
|
18
18
|
* Both consumers call `sweepActiveReactions`, which is shaped as a
|
|
19
|
-
* pure function that takes the
|
|
19
|
+
* pure function that takes the finalize callback as an argument. That
|
|
20
20
|
* keeps it testable in isolation — the tests pass a fake callback and
|
|
21
21
|
* assert which reactions were visited and whether the sidecar was
|
|
22
22
|
* cleared.
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { readActiveReactions, clearActiveReactions, type ActiveReaction } from "./active-reactions.js";
|
|
26
26
|
|
|
27
|
-
export type
|
|
27
|
+
export type FinalizeReactionFn = (chatId: string, messageId: number) => Promise<unknown>;
|
|
28
28
|
|
|
29
29
|
export interface SweepOptions {
|
|
30
30
|
timeoutMs?: number;
|
|
@@ -45,7 +45,7 @@ export interface SweepResult {
|
|
|
45
45
|
*/
|
|
46
46
|
export async function sweepActiveReactions(
|
|
47
47
|
agentDir: string,
|
|
48
|
-
|
|
48
|
+
finalize: FinalizeReactionFn,
|
|
49
49
|
options: SweepOptions = {},
|
|
50
50
|
): Promise<SweepResult> {
|
|
51
51
|
const log = options.log ?? (() => {});
|
|
@@ -56,7 +56,7 @@ export async function sweepActiveReactions(
|
|
|
56
56
|
log(`sweeping ${reactions.length} stale reaction(s)`);
|
|
57
57
|
const attempts = reactions.map((r) =>
|
|
58
58
|
Promise.resolve()
|
|
59
|
-
.then(() =>
|
|
59
|
+
.then(() => finalize(r.chatId, r.messageId))
|
|
60
60
|
.catch((err: unknown) => {
|
|
61
61
|
const msg = err instanceof Error ? err.message : String(err);
|
|
62
62
|
log(`reaction sweep failed for ${r.chatId}/${r.messageId}: ${msg}`);
|
|
@@ -27230,7 +27230,7 @@ var init_secretlint_source = __esm(() => {
|
|
|
27230
27230
|
function escapeHtml8(s) {
|
|
27231
27231
|
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
27232
27232
|
}
|
|
27233
|
-
function
|
|
27233
|
+
function truncate4(s, n) {
|
|
27234
27234
|
return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
|
|
27235
27235
|
}
|
|
27236
27236
|
|
|
@@ -30763,7 +30763,6 @@ var REACTION_VARIANTS = {
|
|
|
30763
30763
|
web: ["\u26a1", "\uD83E\uDD14", "\uD83D\uDC4C"],
|
|
30764
30764
|
compacting: ["\u270d", "\uD83E\uDD14", "\uD83D\uDC40"],
|
|
30765
30765
|
done: ["\uD83D\uDC4D", "\uD83D\uDCAF", "\uD83C\uDF89"],
|
|
30766
|
-
silent: ["\uD83D\uDE4A", "\uD83E\uDD14", "\uD83D\uDE10"],
|
|
30767
30766
|
error: ["\uD83D\uDE31", "\uD83D\uDE28", "\uD83E\uDD2F"],
|
|
30768
30767
|
stallSoft: ["\uD83E\uDD71", "\uD83D\uDE34", "\uD83E\uDD14"],
|
|
30769
30768
|
stallHard: ["\uD83D\uDE28", "\uD83E\uDD2F", "\uD83D\uDE31"]
|
|
@@ -30796,7 +30795,7 @@ class StatusReactionController {
|
|
|
30796
30795
|
constructor(emit, allowedReactions = null, config = {}) {
|
|
30797
30796
|
this.emit = emit;
|
|
30798
30797
|
this.allowedReactions = allowedReactions;
|
|
30799
|
-
this.debounceMs = config.debounceMs ??
|
|
30798
|
+
this.debounceMs = config.debounceMs ?? 3500;
|
|
30800
30799
|
this.stallSoftMs = config.stallSoftMs ?? 30000;
|
|
30801
30800
|
this.stallHardMs = config.stallHardMs ?? 90000;
|
|
30802
30801
|
this.log = config.log;
|
|
@@ -30814,14 +30813,15 @@ class StatusReactionController {
|
|
|
30814
30813
|
setCompacting() {
|
|
30815
30814
|
this.scheduleState("compacting");
|
|
30816
30815
|
}
|
|
30817
|
-
|
|
30818
|
-
this.
|
|
30816
|
+
setError() {
|
|
30817
|
+
this.scheduleState("error");
|
|
30819
30818
|
}
|
|
30820
|
-
|
|
30821
|
-
|
|
30819
|
+
finalize(reason = "done") {
|
|
30820
|
+
const state = reason === "error" ? "error" : "done";
|
|
30821
|
+
this.finishWithState(state);
|
|
30822
30822
|
}
|
|
30823
|
-
|
|
30824
|
-
this.
|
|
30823
|
+
setDone() {
|
|
30824
|
+
this.finalize("done");
|
|
30825
30825
|
}
|
|
30826
30826
|
cancel() {
|
|
30827
30827
|
if (this.finished)
|
|
@@ -32025,15 +32025,6 @@ async function handleStreamReply(args, state, deps) {
|
|
|
32025
32025
|
await stream.finalize();
|
|
32026
32026
|
state.activeDraftStreams.delete(sKey);
|
|
32027
32027
|
state.activeDraftParseModes?.delete(sKey);
|
|
32028
|
-
const isDefaultLaneForCompletion = args.lane == null || args.lane.length === 0;
|
|
32029
|
-
if (isDefaultLaneForCompletion && stream.getMessageId() != null) {
|
|
32030
|
-
try {
|
|
32031
|
-
deps.endStatusReaction(chat_id, threadId, "done");
|
|
32032
|
-
} catch (err) {
|
|
32033
|
-
deps.writeError(`telegram channel: stream_reply: endStatusReaction hook threw: ${err}
|
|
32034
|
-
`);
|
|
32035
|
-
}
|
|
32036
|
-
}
|
|
32037
32028
|
if (stream.getMessageId() == null) {
|
|
32038
32029
|
throw new Error(`stream_reply finalized without sending any message (length=${rawText.length}, ` + `max=4096). Telegram's per-message limit is 4096 chars and stream_reply does not ` + `auto-chunk. Split the text or use \`reply\` (which chunks).`);
|
|
32039
32030
|
}
|
|
@@ -41139,14 +41130,14 @@ function clearActiveReactions2(agentDir) {
|
|
|
41139
41130
|
}
|
|
41140
41131
|
|
|
41141
41132
|
// active-reactions-sweep.ts
|
|
41142
|
-
async function sweepActiveReactions(agentDir,
|
|
41133
|
+
async function sweepActiveReactions(agentDir, finalize, options = {}) {
|
|
41143
41134
|
const log = options.log ?? (() => {});
|
|
41144
41135
|
const timeoutMs = options.timeoutMs ?? 2000;
|
|
41145
41136
|
const reactions = readActiveReactions2(agentDir);
|
|
41146
41137
|
if (reactions.length === 0)
|
|
41147
41138
|
return { swept: [], timedOut: false };
|
|
41148
41139
|
log(`sweeping ${reactions.length} stale reaction(s)`);
|
|
41149
|
-
const attempts = reactions.map((r) => Promise.resolve().then(() =>
|
|
41140
|
+
const attempts = reactions.map((r) => Promise.resolve().then(() => finalize(r.chatId, r.messageId)).catch((err) => {
|
|
41150
41141
|
const msg = err instanceof Error ? err.message : String(err);
|
|
41151
41142
|
log(`reaction sweep failed for ${r.chatId}/${r.messageId}: ${msg}`);
|
|
41152
41143
|
}));
|
|
@@ -41181,7 +41172,7 @@ function flushOnAgentDisconnect(deps) {
|
|
|
41181
41172
|
return false;
|
|
41182
41173
|
}
|
|
41183
41174
|
for (const [key, ctrl] of activeStatusReactions.entries()) {
|
|
41184
|
-
ctrl.
|
|
41175
|
+
ctrl.finalize("done");
|
|
41185
41176
|
activeStatusReactions.delete(key);
|
|
41186
41177
|
activeReactionMsgIds.delete(key);
|
|
41187
41178
|
activeTurnStartedAt.delete(key);
|
|
@@ -41193,7 +41184,7 @@ function flushOnAgentDisconnect(deps) {
|
|
|
41193
41184
|
activeTurnStartedAt.delete(k);
|
|
41194
41185
|
activeReactionMsgIds.delete(k);
|
|
41195
41186
|
}
|
|
41196
|
-
log(`telegram gateway: disconnect-flush swept ${danglingKeys.length} dangling turn key(s) ` + `post-bridge-death (controller loop missed \u2014
|
|
41187
|
+
log(`telegram gateway: disconnect-flush swept ${danglingKeys.length} dangling turn key(s) ` + `post-bridge-death (controller loop missed \u2014 finalize raced disconnect)`);
|
|
41197
41188
|
onDanglingTurnsSwept?.(danglingKeys);
|
|
41198
41189
|
}
|
|
41199
41190
|
disposeProgressDriver();
|
|
@@ -44315,6 +44306,12 @@ function createPendingInboundBuffer(opts = {}) {
|
|
|
44315
44306
|
|
|
44316
44307
|
// gateway/inbound-spool.ts
|
|
44317
44308
|
function spoolId(msg) {
|
|
44309
|
+
if (msg.meta?.source === "subagent_handback" && typeof msg.meta?.subagent_jsonl_id === "string" && msg.meta.subagent_jsonl_id.length > 0) {
|
|
44310
|
+
return `s:handback:${msg.meta.subagent_jsonl_id}`;
|
|
44311
|
+
}
|
|
44312
|
+
if (msg.meta?.source === "subagent_progress" && typeof msg.meta?.subagent_jsonl_id === "string" && msg.meta.subagent_jsonl_id.length > 0 && typeof msg.meta?.bucket_idx === "string" && msg.meta.bucket_idx.length > 0) {
|
|
44313
|
+
return `s:progress:${msg.meta.subagent_jsonl_id}:${msg.meta.bucket_idx}`;
|
|
44314
|
+
}
|
|
44318
44315
|
if (typeof msg.messageId === "number" && msg.messageId > 0) {
|
|
44319
44316
|
return `m:${msg.chatId}:${msg.messageId}`;
|
|
44320
44317
|
}
|
|
@@ -44437,7 +44434,31 @@ function createInboundSpool(opts) {
|
|
|
44437
44434
|
maybeCompact();
|
|
44438
44435
|
},
|
|
44439
44436
|
liveEntries() {
|
|
44440
|
-
|
|
44437
|
+
const cutoff = now();
|
|
44438
|
+
const out = [];
|
|
44439
|
+
for (const e of live.values()) {
|
|
44440
|
+
const expRaw = e.msg.meta?.expiresAt;
|
|
44441
|
+
if (typeof expRaw === "string" && expRaw.length > 0) {
|
|
44442
|
+
const exp = Number(expRaw);
|
|
44443
|
+
if (Number.isFinite(exp) && exp <= cutoff)
|
|
44444
|
+
continue;
|
|
44445
|
+
}
|
|
44446
|
+
out.push({ agent: e.agent, msg: e.msg });
|
|
44447
|
+
}
|
|
44448
|
+
return out;
|
|
44449
|
+
},
|
|
44450
|
+
dropMatching(predicate) {
|
|
44451
|
+
let n = 0;
|
|
44452
|
+
for (const [id, _e] of [...live.entries()]) {
|
|
44453
|
+
if (!predicate(id))
|
|
44454
|
+
continue;
|
|
44455
|
+
live.delete(id);
|
|
44456
|
+
appendRecord({ t: "ack", id });
|
|
44457
|
+
n++;
|
|
44458
|
+
}
|
|
44459
|
+
if (n > 0)
|
|
44460
|
+
maybeCompact();
|
|
44461
|
+
return n;
|
|
44441
44462
|
},
|
|
44442
44463
|
sweepEscalations(onEscalate) {
|
|
44443
44464
|
const cutoff = now() - escalateAfterMs;
|
|
@@ -45082,7 +45103,8 @@ ${result}
|
|
|
45082
45103
|
text,
|
|
45083
45104
|
meta: {
|
|
45084
45105
|
source: "subagent_handback",
|
|
45085
|
-
outcome: opts.ctx.outcome
|
|
45106
|
+
outcome: opts.ctx.outcome,
|
|
45107
|
+
...opts.ctx.jsonlAgentId ? { subagent_jsonl_id: opts.ctx.jsonlAgentId } : {}
|
|
45086
45108
|
}
|
|
45087
45109
|
};
|
|
45088
45110
|
}
|
|
@@ -45105,13 +45127,110 @@ function decideSubagentHandback(input) {
|
|
|
45105
45127
|
chatId,
|
|
45106
45128
|
taskDescription: input.taskDescription,
|
|
45107
45129
|
resultText: input.resultText,
|
|
45108
|
-
outcome: input.outcome
|
|
45130
|
+
outcome: input.outcome,
|
|
45131
|
+
...input.jsonlAgentId ? { jsonlAgentId: input.jsonlAgentId } : {}
|
|
45109
45132
|
},
|
|
45110
45133
|
...input.nowMs !== undefined ? { nowMs: input.nowMs } : {}
|
|
45111
45134
|
});
|
|
45112
45135
|
return { deliver: true, chatId, inbound };
|
|
45113
45136
|
}
|
|
45114
45137
|
|
|
45138
|
+
// gateway/subagent-progress-inbound-builder.ts
|
|
45139
|
+
var PROGRESS_RESULT_MAX = 800;
|
|
45140
|
+
var PROGRESS_DESC_MAX = 200;
|
|
45141
|
+
var DEFAULT_PROGRESS_INTERVAL_MS = 5 * 60 * 1000;
|
|
45142
|
+
function truncate3(s, max) {
|
|
45143
|
+
const t = s.trim();
|
|
45144
|
+
return t.length > max ? t.slice(0, max) + "\u2026" : t;
|
|
45145
|
+
}
|
|
45146
|
+
function formatElapsed(ms) {
|
|
45147
|
+
const totalMin = Math.floor(ms / 60000);
|
|
45148
|
+
if (totalMin < 60)
|
|
45149
|
+
return `${Math.max(1, totalMin)}m`;
|
|
45150
|
+
const h = Math.floor(totalMin / 60);
|
|
45151
|
+
const m = totalMin % 60;
|
|
45152
|
+
return m === 0 ? `${h}h` : `${h}h${m}m`;
|
|
45153
|
+
}
|
|
45154
|
+
function buildSubagentProgressInbound(opts) {
|
|
45155
|
+
const ts = opts.nowMs ?? Date.now();
|
|
45156
|
+
const desc = truncate3(opts.ctx.taskDescription, PROGRESS_DESC_MAX) || "(no description)";
|
|
45157
|
+
const summary = truncate3(opts.ctx.latestSummary, PROGRESS_RESULT_MAX);
|
|
45158
|
+
const elapsed = formatElapsed(opts.ctx.elapsedMs);
|
|
45159
|
+
const expiresAt = ts + 2 * opts.ctx.progressIntervalMs;
|
|
45160
|
+
const text = `\uD83D\uDD04 A background worker you dispatched is still running.
|
|
45161
|
+
|
|
45162
|
+
` + `Task: ${desc}
|
|
45163
|
+
` + `Elapsed: ${elapsed}
|
|
45164
|
+
|
|
45165
|
+
` + (summary ? `Latest activity:
|
|
45166
|
+
${summary}
|
|
45167
|
+
|
|
45168
|
+
` : `(no narrative line yet \u2014 worker has been tool-only)
|
|
45169
|
+
|
|
45170
|
+
`) + `This is beat 3 \u2014 mid-flight progress. Surface ONE short line to ` + `the user in your own voice about what the worker is up to. Do ` + `NOT paste this raw, do NOT repeat the elapsed time verbatim, do ` + `NOT promise completion. The handback (beat 4) will come ` + `separately when the worker finishes.`;
|
|
45171
|
+
return {
|
|
45172
|
+
type: "inbound",
|
|
45173
|
+
chatId: opts.ctx.chatId,
|
|
45174
|
+
messageId: ts,
|
|
45175
|
+
user: "subagent-watcher",
|
|
45176
|
+
userId: 0,
|
|
45177
|
+
ts,
|
|
45178
|
+
text,
|
|
45179
|
+
meta: {
|
|
45180
|
+
source: "subagent_progress",
|
|
45181
|
+
subagent_jsonl_id: opts.ctx.subagentJsonlId,
|
|
45182
|
+
bucket_idx: String(opts.ctx.bucketIdx),
|
|
45183
|
+
expiresAt: String(expiresAt),
|
|
45184
|
+
elapsed_ms: String(opts.ctx.elapsedMs)
|
|
45185
|
+
}
|
|
45186
|
+
};
|
|
45187
|
+
}
|
|
45188
|
+
function isEnvFlagOn(value) {
|
|
45189
|
+
if (value == null)
|
|
45190
|
+
return false;
|
|
45191
|
+
const v = value.trim().toLowerCase();
|
|
45192
|
+
if (v === "")
|
|
45193
|
+
return false;
|
|
45194
|
+
if (v === "0" || v === "false" || v === "no" || v === "off")
|
|
45195
|
+
return false;
|
|
45196
|
+
return true;
|
|
45197
|
+
}
|
|
45198
|
+
function decideSubagentProgress(input) {
|
|
45199
|
+
if (isEnvFlagOn(input.disableEnvValue)) {
|
|
45200
|
+
return { deliver: false, reason: "env-disabled" };
|
|
45201
|
+
}
|
|
45202
|
+
if (!input.isBackground) {
|
|
45203
|
+
return { deliver: false, reason: "foreground" };
|
|
45204
|
+
}
|
|
45205
|
+
const chatId = input.fleetChatId || input.ownerChatId;
|
|
45206
|
+
if (!chatId) {
|
|
45207
|
+
return { deliver: false, reason: "no-chat" };
|
|
45208
|
+
}
|
|
45209
|
+
if (!input.subagentJsonlId) {
|
|
45210
|
+
return { deliver: false, reason: "missing-jsonl-id" };
|
|
45211
|
+
}
|
|
45212
|
+
const bucketIdx = Math.floor(input.elapsedMs / input.progressIntervalMs);
|
|
45213
|
+
if (bucketIdx < 1) {
|
|
45214
|
+
return { deliver: false, reason: "first-bucket-suppressed" };
|
|
45215
|
+
}
|
|
45216
|
+
if (input.lastBucketIdx != null && bucketIdx <= input.lastBucketIdx) {
|
|
45217
|
+
return { deliver: false, reason: "bucket-already-fired" };
|
|
45218
|
+
}
|
|
45219
|
+
const inbound = buildSubagentProgressInbound({
|
|
45220
|
+
ctx: {
|
|
45221
|
+
chatId,
|
|
45222
|
+
subagentJsonlId: input.subagentJsonlId,
|
|
45223
|
+
taskDescription: input.taskDescription,
|
|
45224
|
+
latestSummary: input.latestSummary,
|
|
45225
|
+
elapsedMs: input.elapsedMs,
|
|
45226
|
+
bucketIdx,
|
|
45227
|
+
progressIntervalMs: input.progressIntervalMs
|
|
45228
|
+
},
|
|
45229
|
+
...input.nowMs !== undefined ? { nowMs: input.nowMs } : {}
|
|
45230
|
+
});
|
|
45231
|
+
return { deliver: true, chatId, bucketIdx, inbound };
|
|
45232
|
+
}
|
|
45233
|
+
|
|
45115
45234
|
// gateway/poll-health.ts
|
|
45116
45235
|
var DEFAULT_LOG = (msg) => {
|
|
45117
45236
|
process.stderr.write(msg.endsWith(`
|
|
@@ -46733,7 +46852,7 @@ function backfillJsonlAgentId(db2, jsonlPath, agentId, log) {
|
|
|
46733
46852
|
db2.prepare("UPDATE subagents SET jsonl_agent_id = ? WHERE id = ?").run(agentId, candidate.id);
|
|
46734
46853
|
log?.(`subagent-watcher: backfill linked ${agentId} \u2192 ${candidate.id}`);
|
|
46735
46854
|
}
|
|
46736
|
-
function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, parentStateDir, onUnstall, onFileVanished) {
|
|
46855
|
+
function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, parentStateDir, onUnstall, onFileVanished, onProgress) {
|
|
46737
46856
|
try {
|
|
46738
46857
|
const stat = fs2.statSync(entry.filePath);
|
|
46739
46858
|
if (stat.size < tail.cursor) {
|
|
@@ -46815,6 +46934,22 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
|
|
|
46815
46934
|
entry.lastSummaryLine = ev.text.split(`
|
|
46816
46935
|
`)[0].trim().slice(0, 120);
|
|
46817
46936
|
entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX);
|
|
46937
|
+
if (onProgress != null && entry.state === "running" && !entry.historical) {
|
|
46938
|
+
try {
|
|
46939
|
+
onProgress({
|
|
46940
|
+
agentId: entry.agentId,
|
|
46941
|
+
description: entry.description,
|
|
46942
|
+
latestSummary: entry.lastResultText,
|
|
46943
|
+
elapsedMs: now - entry.dispatchedAt,
|
|
46944
|
+
prevBucketIdx: entry.lastProgressBucketIdx,
|
|
46945
|
+
setBucketIdx: (b) => {
|
|
46946
|
+
entry.lastProgressBucketIdx = b;
|
|
46947
|
+
}
|
|
46948
|
+
});
|
|
46949
|
+
} catch (cbErr) {
|
|
46950
|
+
log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${cbErr.message}`);
|
|
46951
|
+
}
|
|
46952
|
+
}
|
|
46818
46953
|
} else if (ev.kind === "sub_agent_turn_end") {
|
|
46819
46954
|
if (entry.state === "running") {
|
|
46820
46955
|
entry.state = "done";
|
|
@@ -46913,6 +47048,7 @@ function startSubagentWatcher(config) {
|
|
|
46913
47048
|
stallTerminalSynthesised: false,
|
|
46914
47049
|
lastSummaryLine: "",
|
|
46915
47050
|
lastResultText: "",
|
|
47051
|
+
lastProgressBucketIdx: null,
|
|
46916
47052
|
lastTool: null,
|
|
46917
47053
|
historical: isHistorical
|
|
46918
47054
|
};
|
|
@@ -46933,7 +47069,7 @@ function startSubagentWatcher(config) {
|
|
|
46933
47069
|
tails.set(agentId, tail);
|
|
46934
47070
|
readSubTail(entry, tail, n, (desc) => {
|
|
46935
47071
|
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`);
|
|
46936
|
-
}, fs2, log, db2, parentStateDir, config.onUnstall);
|
|
47072
|
+
}, fs2, log, db2, parentStateDir, config.onUnstall, undefined, config.onProgress);
|
|
46937
47073
|
if (isHistorical && entry.state === "done") {
|
|
46938
47074
|
entry.completionNotified = true;
|
|
46939
47075
|
scheduleTerminalCleanup(agentId);
|
|
@@ -46950,7 +47086,7 @@ function startSubagentWatcher(config) {
|
|
|
46950
47086
|
return;
|
|
46951
47087
|
readSubTail(entry2, t, nowFn(), (desc) => {
|
|
46952
47088
|
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`);
|
|
46953
|
-
}, fs2, log, db2, parentStateDir, config.onUnstall, cleanupTerminalAgent);
|
|
47089
|
+
}, fs2, log, db2, parentStateDir, config.onUnstall, cleanupTerminalAgent, config.onProgress);
|
|
46954
47090
|
maybySendStateTransition(agentId);
|
|
46955
47091
|
});
|
|
46956
47092
|
} catch (err) {
|
|
@@ -47044,7 +47180,7 @@ function startSubagentWatcher(config) {
|
|
|
47044
47180
|
if (idleMs >= threshold) {
|
|
47045
47181
|
entry.stallNotified = true;
|
|
47046
47182
|
entry.stalledAt = n;
|
|
47047
|
-
const desc = escapeHtml8(
|
|
47183
|
+
const desc = escapeHtml8(truncate4(entry.description, 80));
|
|
47048
47184
|
const idleSec = Math.floor(idleMs / 1000);
|
|
47049
47185
|
log?.(`subagent-watcher: stall detected for ${entry.agentId} (idle ${idleSec}s): ${desc}`);
|
|
47050
47186
|
if (db2 != null) {
|
|
@@ -47196,7 +47332,7 @@ function startSubagentWatcher(config) {
|
|
|
47196
47332
|
continue;
|
|
47197
47333
|
readSubTail(entry, tail, n, (desc) => {
|
|
47198
47334
|
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`);
|
|
47199
|
-
}, fs2, log, db2, parentStateDir, config.onUnstall, cleanupTerminalAgent);
|
|
47335
|
+
}, fs2, log, db2, parentStateDir, config.onUnstall, cleanupTerminalAgent, config.onProgress);
|
|
47200
47336
|
maybySendStateTransition(agentId);
|
|
47201
47337
|
}
|
|
47202
47338
|
checkStalls();
|
|
@@ -48006,7 +48142,7 @@ function summarizeToolForTitle(toolName, inputPreview) {
|
|
|
48006
48142
|
}
|
|
48007
48143
|
case "Bash": {
|
|
48008
48144
|
const command = readString(input, "command");
|
|
48009
|
-
return command ? `${toolName}: ${
|
|
48145
|
+
return command ? `${toolName}: ${truncate5(command, COMMAND_TITLE_MAX)}` : toolName;
|
|
48010
48146
|
}
|
|
48011
48147
|
case "Read":
|
|
48012
48148
|
case "Edit":
|
|
@@ -48014,17 +48150,17 @@ function summarizeToolForTitle(toolName, inputPreview) {
|
|
|
48014
48150
|
case "MultiEdit":
|
|
48015
48151
|
case "NotebookEdit": {
|
|
48016
48152
|
const filePath = readString(input, "file_path") ?? readString(input, "notebook_path");
|
|
48017
|
-
return filePath ? `${toolName}: ${
|
|
48153
|
+
return filePath ? `${toolName}: ${truncate5(basename5(filePath), PATH_TITLE_MAX)}` : toolName;
|
|
48018
48154
|
}
|
|
48019
48155
|
case "Glob":
|
|
48020
48156
|
case "Grep": {
|
|
48021
48157
|
const pattern = readString(input, "pattern");
|
|
48022
|
-
return pattern ? `${toolName}: ${
|
|
48158
|
+
return pattern ? `${toolName}: ${truncate5(pattern, COMMAND_TITLE_MAX)}` : toolName;
|
|
48023
48159
|
}
|
|
48024
48160
|
case "WebFetch":
|
|
48025
48161
|
case "WebSearch": {
|
|
48026
48162
|
const query2 = readString(input, "url") ?? readString(input, "query");
|
|
48027
|
-
return query2 ? `${toolName}: ${
|
|
48163
|
+
return query2 ? `${toolName}: ${truncate5(query2, COMMAND_TITLE_MAX)}` : toolName;
|
|
48028
48164
|
}
|
|
48029
48165
|
default:
|
|
48030
48166
|
return toolName;
|
|
@@ -48057,7 +48193,7 @@ function skillBasenameFromPath(input) {
|
|
|
48057
48193
|
const basename6 = lastSlash >= 0 ? trimmed.slice(lastSlash + 1) : trimmed;
|
|
48058
48194
|
return basename6.length > 0 ? basename6 : null;
|
|
48059
48195
|
}
|
|
48060
|
-
function
|
|
48196
|
+
function truncate5(text, max) {
|
|
48061
48197
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
48062
48198
|
if (collapsed.length <= max)
|
|
48063
48199
|
return collapsed;
|
|
@@ -48328,10 +48464,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
48328
48464
|
}
|
|
48329
48465
|
|
|
48330
48466
|
// ../src/build-info.ts
|
|
48331
|
-
var VERSION = "0.13.
|
|
48332
|
-
var COMMIT_SHA = "
|
|
48333
|
-
var COMMIT_DATE = "2026-05-
|
|
48334
|
-
var LATEST_PR =
|
|
48467
|
+
var VERSION = "0.13.27";
|
|
48468
|
+
var COMMIT_SHA = "a158e029";
|
|
48469
|
+
var COMMIT_DATE = "2026-05-24T09:47:16Z";
|
|
48470
|
+
var LATEST_PR = 1727;
|
|
48335
48471
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
48336
48472
|
|
|
48337
48473
|
// gateway/boot-version.ts
|
|
@@ -49362,6 +49498,9 @@ function maybeProactiveCompact() {
|
|
|
49362
49498
|
resolveCompactCard("superseded", occupancy);
|
|
49363
49499
|
}
|
|
49364
49500
|
postCompactCard(occupancy, cap);
|
|
49501
|
+
for (const ctrl of activeStatusReactions.values()) {
|
|
49502
|
+
ctrl.setCompacting();
|
|
49503
|
+
}
|
|
49365
49504
|
}
|
|
49366
49505
|
if (!decision.fire)
|
|
49367
49506
|
return;
|
|
@@ -49435,15 +49574,12 @@ async function resolveCompactCard(kind, occNow) {
|
|
|
49435
49574
|
`);
|
|
49436
49575
|
}
|
|
49437
49576
|
}
|
|
49438
|
-
function
|
|
49577
|
+
function finalizeStatusReaction(chatId, threadId, reason = "done") {
|
|
49439
49578
|
const key = statusKey(chatId, threadId);
|
|
49440
49579
|
const ctrl = activeStatusReactions.get(key);
|
|
49441
49580
|
if (!ctrl)
|
|
49442
49581
|
return;
|
|
49443
|
-
|
|
49444
|
-
ctrl.setDone();
|
|
49445
|
-
else
|
|
49446
|
-
ctrl.setError();
|
|
49582
|
+
ctrl.finalize(reason);
|
|
49447
49583
|
purgeReactionTracking(key);
|
|
49448
49584
|
}
|
|
49449
49585
|
function resolveThreadId(chat_id, explicit) {
|
|
@@ -51188,12 +51324,6 @@ ${url}`;
|
|
|
51188
51324
|
progressDriver?.recordOutboundDelivered(chat_id, threadId != null ? String(threadId) : undefined);
|
|
51189
51325
|
} catch {}
|
|
51190
51326
|
noteSignal(statusKey(chat_id, threadId), Date.now());
|
|
51191
|
-
try {
|
|
51192
|
-
endStatusReaction(chat_id, threadId, "done");
|
|
51193
|
-
} catch (err) {
|
|
51194
|
-
process.stderr.write(`telegram gateway: reply: endStatusReaction hook threw: ${err}
|
|
51195
|
-
`);
|
|
51196
|
-
}
|
|
51197
51327
|
if (turn != null && isFinalAnswerReply({ text: rawText, disableNotification })) {
|
|
51198
51328
|
turn.finalAnswerDelivered = true;
|
|
51199
51329
|
}
|
|
@@ -51284,7 +51414,6 @@ async function executeStreamReply(args) {
|
|
|
51284
51414
|
disableLinkPreview: access.disableLinkPreview !== false,
|
|
51285
51415
|
defaultFormat: access.parseMode ?? "html",
|
|
51286
51416
|
logStreamingEvent,
|
|
51287
|
-
endStatusReaction,
|
|
51288
51417
|
isPrivateChat: streamIsPrivate,
|
|
51289
51418
|
isForumTopic: streamIsForumTopic,
|
|
51290
51419
|
...sendMessageDraftFn != null ? { sendMessageDraft: sendMessageDraftFn } : {},
|
|
@@ -52274,11 +52403,8 @@ function handleSessionEvent(ev) {
|
|
|
52274
52403
|
verb: "context-exhaust-warning",
|
|
52275
52404
|
...threadId != null ? { threadId } : {}
|
|
52276
52405
|
});
|
|
52406
|
+
finalizeStatusReaction(chatId, threadId, "error");
|
|
52277
52407
|
const ceKey = statusKey(chatId, threadId);
|
|
52278
|
-
const ctrl = activeStatusReactions.get(ceKey);
|
|
52279
|
-
if (ctrl)
|
|
52280
|
-
ctrl.setError();
|
|
52281
|
-
purgeReactionTracking(ceKey);
|
|
52282
52408
|
endTurn(ceKey);
|
|
52283
52409
|
noteTurnEnd(ceKey);
|
|
52284
52410
|
if (turn.answerStream != null) {
|
|
@@ -52380,9 +52506,7 @@ function handleSessionEvent(ev) {
|
|
|
52380
52506
|
}
|
|
52381
52507
|
}
|
|
52382
52508
|
unpinProgressCardForChat?.(chatId, threadId);
|
|
52383
|
-
|
|
52384
|
-
ctrl.setDone();
|
|
52385
|
-
purgeReactionTracking(statusKey(chatId, threadId));
|
|
52509
|
+
finalizeStatusReaction(chatId, threadId, "done");
|
|
52386
52510
|
{
|
|
52387
52511
|
const sKey = streamKey3(chatId, threadId);
|
|
52388
52512
|
const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
|
|
@@ -52537,7 +52661,7 @@ function handleSessionEvent(ev) {
|
|
|
52537
52661
|
}
|
|
52538
52662
|
outboundDedup.record(backstopChatId, backstopThreadId, capturedText, Date.now(), currentTurn?.registryKey ?? null);
|
|
52539
52663
|
if (backstopCtrl)
|
|
52540
|
-
backstopCtrl.
|
|
52664
|
+
backstopCtrl.finalize("done");
|
|
52541
52665
|
if (backstopCardTurnKey != null) {
|
|
52542
52666
|
completeProgressCardTurn?.({
|
|
52543
52667
|
chatId: backstopChatId,
|
|
@@ -52551,16 +52675,14 @@ function handleSessionEvent(ev) {
|
|
|
52551
52675
|
process.stderr.write(`telegram gateway: turn-flush send failed: ${err.message}
|
|
52552
52676
|
`);
|
|
52553
52677
|
if (backstopCtrl)
|
|
52554
|
-
backstopCtrl.
|
|
52678
|
+
backstopCtrl.finalize("error");
|
|
52555
52679
|
} finally {
|
|
52556
52680
|
purgeReactionTracking(statusKey(backstopChatId, backstopThreadId));
|
|
52557
52681
|
}
|
|
52558
52682
|
})();
|
|
52559
52683
|
return;
|
|
52560
52684
|
}
|
|
52561
|
-
|
|
52562
|
-
ctrl.setDone();
|
|
52563
|
-
purgeReactionTracking(statusKey(chatId, threadId));
|
|
52685
|
+
finalizeStatusReaction(chatId, threadId, "done");
|
|
52564
52686
|
{
|
|
52565
52687
|
const sKey = streamKey3(chatId, threadId);
|
|
52566
52688
|
const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
|
|
@@ -57918,7 +58040,8 @@ var didOneTimeSetup = false;
|
|
|
57918
58040
|
fleetChatId,
|
|
57919
58041
|
ownerChatId: loadAccess().allowFrom[0] ?? "",
|
|
57920
58042
|
taskDescription: description,
|
|
57921
|
-
resultText
|
|
58043
|
+
resultText,
|
|
58044
|
+
jsonlAgentId: agentId
|
|
57922
58045
|
});
|
|
57923
58046
|
if (!decision.deliver) {
|
|
57924
58047
|
if (decision.reason === "no-chat") {
|
|
@@ -57927,8 +58050,60 @@ var didOneTimeSetup = false;
|
|
|
57927
58050
|
}
|
|
57928
58051
|
return;
|
|
57929
58052
|
}
|
|
58053
|
+
try {
|
|
58054
|
+
const progressPrefix = `s:progress:${agentId}:`;
|
|
58055
|
+
const dropped = inboundSpool?.dropMatching((id) => id.startsWith(progressPrefix)) ?? 0;
|
|
58056
|
+
if (dropped > 0) {
|
|
58057
|
+
process.stderr.write(`telegram gateway: subagent-handback ${agentId} swept ${dropped} live progress envelope(s) from spool
|
|
58058
|
+
`);
|
|
58059
|
+
}
|
|
58060
|
+
} catch (err) {
|
|
58061
|
+
process.stderr.write(`telegram gateway: subagent-handback ${agentId} progress-sweep error: ${err.message}
|
|
58062
|
+
`);
|
|
58063
|
+
}
|
|
57930
58064
|
pendingInboundBuffer.push(process.env.SWITCHROOM_AGENT_NAME ?? "", decision.inbound);
|
|
57931
58065
|
process.stderr.write(`telegram gateway: subagent-handback queued agent=${agentId} outcome=${outcome} chat=${decision.chatId} resultChars=${resultText.length}
|
|
58066
|
+
`);
|
|
58067
|
+
},
|
|
58068
|
+
onProgress: ({ agentId, description, latestSummary, elapsedMs, prevBucketIdx, setBucketIdx }) => {
|
|
58069
|
+
let fleetChatId = "";
|
|
58070
|
+
let isBackground = false;
|
|
58071
|
+
try {
|
|
58072
|
+
const fleets = progressDriver?.peekAllFleets() ?? [];
|
|
58073
|
+
for (const f of fleets) {
|
|
58074
|
+
if (f.fleet.has(agentId)) {
|
|
58075
|
+
fleetChatId = f.chatId ?? "";
|
|
58076
|
+
break;
|
|
58077
|
+
}
|
|
58078
|
+
}
|
|
58079
|
+
} catch {}
|
|
58080
|
+
if (turnsDb != null) {
|
|
58081
|
+
try {
|
|
58082
|
+
const row = turnsDb.prepare("SELECT background FROM subagents WHERE jsonl_agent_id = ?").get(agentId);
|
|
58083
|
+
if (row != null)
|
|
58084
|
+
isBackground = row.background === 1;
|
|
58085
|
+
} catch {}
|
|
58086
|
+
}
|
|
58087
|
+
if (!isBackground)
|
|
58088
|
+
return;
|
|
58089
|
+
const decision = decideSubagentProgress({
|
|
58090
|
+
disableEnvValue: process.env.SWITCHROOM_DISABLE_SUBAGENT_PROGRESS,
|
|
58091
|
+
isBackground,
|
|
58092
|
+
fleetChatId,
|
|
58093
|
+
ownerChatId: loadAccess().allowFrom[0] ?? "",
|
|
58094
|
+
subagentJsonlId: agentId,
|
|
58095
|
+
taskDescription: description,
|
|
58096
|
+
latestSummary,
|
|
58097
|
+
elapsedMs,
|
|
58098
|
+
progressIntervalMs: DEFAULT_PROGRESS_INTERVAL_MS,
|
|
58099
|
+
lastBucketIdx: prevBucketIdx
|
|
58100
|
+
});
|
|
58101
|
+
if (!decision.deliver)
|
|
58102
|
+
return;
|
|
58103
|
+
setBucketIdx(decision.bucketIdx);
|
|
58104
|
+
pendingInboundBuffer.push(process.env.SWITCHROOM_AGENT_NAME ?? "", decision.inbound);
|
|
58105
|
+
clearPending(statusKey(decision.chatId, undefined), "progress");
|
|
58106
|
+
process.stderr.write(`telegram gateway: subagent-progress queued agent=${agentId} bucket=${decision.bucketIdx} elapsed_ms=${elapsedMs} chat=${decision.chatId}
|
|
57932
58107
|
`);
|
|
57933
58108
|
}
|
|
57934
58109
|
});
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
# Waiting-for-reply UX — v2 spec (three-class contract)
|
|
2
2
|
|
|
3
3
|
Tracks: [#545](https://github.com/mekenthompson/switchroom/issues/545),
|
|
4
|
-
[#553](https://github.com/mekenthompson/switchroom/issues/553) (PR series)
|
|
4
|
+
[#553](https://github.com/mekenthompson/switchroom/issues/553) (PR series),
|
|
5
|
+
[#1713](https://github.com/switchroom/switchroom/issues/1713)
|
|
6
|
+
(reflective status-reaction restoration)
|
|
7
|
+
|
|
8
|
+
> **#1713 note — defect restoration, not feature change.** The Class B
|
|
9
|
+
> contract below ("Ladder progresses through 🤔 / tool-glyphs … **Must
|
|
10
|
+
> NOT collapse straight to 👍**") already documents the correct
|
|
11
|
+
> reflective behaviour. The implementation had regressed: plain `reply`
|
|
12
|
+
> and `stream_reply done=true` were firing the terminal 👍 mid-turn,
|
|
13
|
+
> collapsing the ladder. #1713 restores the documented contract — the
|
|
14
|
+
> **`turn_end` IPC event (Stop hook) is the sole terminal trigger**.
|
|
15
|
+
> Mid-turn replies (ack OR final-answer) are non-events for the
|
|
16
|
+
> reaction. Working states (🤔 / ✍ / 👨💻 / ⚡ / 🗜) are bidirectional
|
|
17
|
+
> and may re-enter any number of times within a turn. 🔥 (5xx) is also
|
|
18
|
+
> non-terminal: recovery to a working state is allowed; only
|
|
19
|
+
> `finalize()` ends the controller. Controller debounce is 3500ms by
|
|
20
|
+
> default (#1713 spec: 3-5s) to coalesce rapid state flips.
|
|
5
21
|
|
|
6
22
|
This document codifies the user-perceived contract for what happens
|
|
7
23
|
between "I sent a Telegram message" and "the agent's reply is locked
|