@threadbase-sh/streamer 1.53.1 → 1.54.0
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.cjs +113 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +102 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -1
- package/dist/index.d.ts +97 -1
- package/dist/index.js +102 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1114,6 +1114,7 @@ function rememberedGateDigit(gate) {
|
|
|
1114
1114
|
// src/services/questions/codexScreen.ts
|
|
1115
1115
|
var CODEX_PROMPT_READY_TEXT = "Ready";
|
|
1116
1116
|
var CODEX_BUSY_STATUS_RE = /\b(?:Starting|Working)\b/;
|
|
1117
|
+
var CODEX_WORKING_STATUS_RE = /\bWorking\b/;
|
|
1117
1118
|
var CODEX_MCP_BOOT_RE = /Booting MCP|Starting MCP servers/i;
|
|
1118
1119
|
var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
|
|
1119
1120
|
var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
|
|
@@ -1150,12 +1151,15 @@ function detectCodexBlockingPrompt(lines) {
|
|
|
1150
1151
|
function codexStatusBarLine(lines) {
|
|
1151
1152
|
return [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
1152
1153
|
}
|
|
1153
|
-
function
|
|
1154
|
+
function codexScreenPreTurn(lines) {
|
|
1154
1155
|
const screenText = lines.join("\n");
|
|
1155
1156
|
if (CODEX_HOOKS_GATE_REGEX.test(screenText) || CODEX_TRUST_GATE_REGEX.test(screenText)) {
|
|
1156
1157
|
return true;
|
|
1157
1158
|
}
|
|
1158
|
-
|
|
1159
|
+
return CODEX_MCP_BOOT_RE.test(screenText);
|
|
1160
|
+
}
|
|
1161
|
+
function codexScreenBlocksComposer(lines) {
|
|
1162
|
+
if (codexScreenPreTurn(lines)) return true;
|
|
1159
1163
|
return CODEX_BUSY_STATUS_RE.test(codexStatusBarLine(lines));
|
|
1160
1164
|
}
|
|
1161
1165
|
function codexScreenShowsReady(lines) {
|
|
@@ -1202,6 +1206,20 @@ function gateCard(gate, lines) {
|
|
|
1202
1206
|
};
|
|
1203
1207
|
}
|
|
1204
1208
|
|
|
1209
|
+
// src/services/questions/parseAgentPhase.ts
|
|
1210
|
+
function codexPhase(lines) {
|
|
1211
|
+
if (codexScreenPreTurn(lines)) return null;
|
|
1212
|
+
return CODEX_WORKING_STATUS_RE.test(codexStatusBarLine(lines)) ? "working" : null;
|
|
1213
|
+
}
|
|
1214
|
+
function claudePhase(_lines) {
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
function parseAgentPhase(lines, provider) {
|
|
1218
|
+
if (provider === CODEX_CLI_PROVIDER) return codexPhase(lines);
|
|
1219
|
+
if (provider === CLAUDE_CODE_PROVIDER) return claudePhase(lines);
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1205
1223
|
// src/utils/debounce.ts
|
|
1206
1224
|
function debounce(fn, waitMs) {
|
|
1207
1225
|
let timer = null;
|
|
@@ -1246,6 +1264,7 @@ var CodexPtyRunner = class {
|
|
|
1246
1264
|
sessions = /* @__PURE__ */ new Map();
|
|
1247
1265
|
onOutput;
|
|
1248
1266
|
onStatusChange;
|
|
1267
|
+
onPhaseChange;
|
|
1249
1268
|
onReady;
|
|
1250
1269
|
// Broadcasts Codex's blocking startup gates (directory trust, hooks review)
|
|
1251
1270
|
// as question cards; null dismisses the card once the gate leaves the screen.
|
|
@@ -1294,6 +1313,7 @@ var CodexPtyRunner = class {
|
|
|
1294
1313
|
constructor(options = {}) {
|
|
1295
1314
|
this.onOutput = options.onOutput;
|
|
1296
1315
|
this.onStatusChange = options.onStatusChange;
|
|
1316
|
+
this.onPhaseChange = options.onPhaseChange;
|
|
1297
1317
|
this.onReady = options.onReady;
|
|
1298
1318
|
this.onPermissionChange = options.onPermissionChange;
|
|
1299
1319
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
@@ -1861,6 +1881,9 @@ var CodexPtyRunner = class {
|
|
|
1861
1881
|
);
|
|
1862
1882
|
return;
|
|
1863
1883
|
}
|
|
1884
|
+
if (session.status === "running") {
|
|
1885
|
+
this.setPhase(sessionId, session, parseAgentPhase(lines, CODEX_CLI_PROVIDER));
|
|
1886
|
+
}
|
|
1864
1887
|
const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
|
|
1865
1888
|
if (gate) {
|
|
1866
1889
|
this.handleGate(sessionId, session, gate, lines);
|
|
@@ -1975,11 +1998,25 @@ var CodexPtyRunner = class {
|
|
|
1975
1998
|
});
|
|
1976
1999
|
this.onPermissionChange?.(sessionId, card);
|
|
1977
2000
|
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Record the agent's phase and notify only on a real change. The guard is
|
|
2003
|
+
* load-bearing, not an optimisation: detectScreenState runs on every chunk,
|
|
2004
|
+
* so an unguarded setter would fire the WS frame behind this several times a
|
|
2005
|
+
* second for a whole turn while reporting the same value. Same contract as
|
|
2006
|
+
* PTYManager.setPhase — the two runners deliberately stay separate classes.
|
|
2007
|
+
*/
|
|
2008
|
+
setPhase(sessionId, session, phase) {
|
|
2009
|
+
const next = phase ?? null;
|
|
2010
|
+
if ((session.subStatus ?? null) === next) return;
|
|
2011
|
+
session.subStatus = next;
|
|
2012
|
+
this.onPhaseChange?.(sessionId, next);
|
|
2013
|
+
}
|
|
1978
2014
|
markReady(sessionId, session, source, reason) {
|
|
1979
2015
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1980
2016
|
session.status = "waiting_input";
|
|
1981
2017
|
session.statusSource = source;
|
|
1982
2018
|
session.statusUpdatedAt = /* @__PURE__ */ new Date();
|
|
2019
|
+
this.setPhase(sessionId, session, null);
|
|
1983
2020
|
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
|
|
1984
2021
|
event: "codex.ready",
|
|
1985
2022
|
sessionId,
|
|
@@ -2071,12 +2108,16 @@ function toPublicSession(s) {
|
|
|
2071
2108
|
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
2072
2109
|
...s.statusSource != null && { statusSource: s.statusSource },
|
|
2073
2110
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
2074
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
2111
|
+
...s.filePath != null && { filePath: s.filePath },
|
|
2112
|
+
// Unconditional — see PTYManager's toPublicSession: a streamer re-adopting
|
|
2113
|
+
// a surviving pty-host's sessions mid-turn has no other source for the
|
|
2114
|
+
// phase, and the host's change guard will not re-emit it.
|
|
2115
|
+
subStatus: s.subStatus ?? null
|
|
2075
2116
|
};
|
|
2076
2117
|
}
|
|
2077
2118
|
|
|
2078
2119
|
// src/pty-host/protocol.ts
|
|
2079
|
-
var PTY_HOST_PROTOCOL_VERSION =
|
|
2120
|
+
var PTY_HOST_PROTOCOL_VERSION = 3;
|
|
2080
2121
|
function isHostEvent(message) {
|
|
2081
2122
|
return "type" in message && message.type === "event";
|
|
2082
2123
|
}
|
|
@@ -2314,6 +2355,9 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2314
2355
|
case "permission-change":
|
|
2315
2356
|
this.options.onPermissionChange?.(event.sessionId, event.gate);
|
|
2316
2357
|
break;
|
|
2358
|
+
case "phase-change":
|
|
2359
|
+
this.options.onPhaseChange?.(event.sessionId, event.phase);
|
|
2360
|
+
break;
|
|
2317
2361
|
case "live-question":
|
|
2318
2362
|
this.options.onLiveQuestion?.(event.sessionId, event.questions);
|
|
2319
2363
|
break;
|
|
@@ -2712,6 +2756,7 @@ var PTYManager = class {
|
|
|
2712
2756
|
onStatusChange;
|
|
2713
2757
|
onReady;
|
|
2714
2758
|
onPermissionChange;
|
|
2759
|
+
onPhaseChange;
|
|
2715
2760
|
onLiveQuestion;
|
|
2716
2761
|
onLiveQuestionGone;
|
|
2717
2762
|
onUserMessage;
|
|
@@ -2773,6 +2818,7 @@ var PTYManager = class {
|
|
|
2773
2818
|
this.onStatusChange = options.onStatusChange;
|
|
2774
2819
|
this.onReady = options.onReady;
|
|
2775
2820
|
this.onPermissionChange = options.onPermissionChange;
|
|
2821
|
+
this.onPhaseChange = options.onPhaseChange;
|
|
2776
2822
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
2777
2823
|
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
2778
2824
|
this.onUserMessage = options.onUserMessage;
|
|
@@ -3279,6 +3325,10 @@ var PTYManager = class {
|
|
|
3279
3325
|
}
|
|
3280
3326
|
this.lastDetectAt.set(sessionId, nowMs);
|
|
3281
3327
|
const lines = await this.getOutputLines(sessionId, 60);
|
|
3328
|
+
const phaseSession = this.sessions.get(sessionId);
|
|
3329
|
+
if (phaseSession?.status === "running") {
|
|
3330
|
+
this.setPhase(sessionId, phaseSession, parseAgentPhase(lines, CLAUDE_CODE_PROVIDER));
|
|
3331
|
+
}
|
|
3282
3332
|
const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
|
|
3283
3333
|
if (oscPermission || hasAskFooter || askFooterOnScreen) {
|
|
3284
3334
|
this.log.debug?.(`[pty.prompt_detect] ${sessionId.slice(0, 8)} trigger`, {
|
|
@@ -3422,12 +3472,27 @@ var PTYManager = class {
|
|
|
3422
3472
|
}
|
|
3423
3473
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
3424
3474
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
3475
|
+
/**
|
|
3476
|
+
* Record the agent's phase and notify only on a real change.
|
|
3477
|
+
*
|
|
3478
|
+
* The change guard is load-bearing, not an optimisation: the scrape pass runs
|
|
3479
|
+
* on every chunk, so an unguarded setter would fire the callback — and the
|
|
3480
|
+
* WS frame behind it — several times a second for the entire duration of a
|
|
3481
|
+
* turn while reporting the same value.
|
|
3482
|
+
*/
|
|
3483
|
+
setPhase(sessionId, session, phase) {
|
|
3484
|
+
const next = phase ?? null;
|
|
3485
|
+
if ((session.subStatus ?? null) === next) return;
|
|
3486
|
+
session.subStatus = next;
|
|
3487
|
+
this.onPhaseChange?.(sessionId, next);
|
|
3488
|
+
}
|
|
3425
3489
|
markReady(sessionId, session, source, reason) {
|
|
3426
3490
|
this.clearReadyFallback(sessionId);
|
|
3427
3491
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
3428
3492
|
session.status = "waiting_input";
|
|
3429
3493
|
session.statusSource = source;
|
|
3430
3494
|
session.statusUpdatedAt = /* @__PURE__ */ new Date();
|
|
3495
|
+
this.setPhase(sessionId, session, null);
|
|
3431
3496
|
const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());
|
|
3432
3497
|
this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {
|
|
3433
3498
|
event: "pty.ready",
|
|
@@ -3491,7 +3556,12 @@ function toPublicSession2(s) {
|
|
|
3491
3556
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
3492
3557
|
...s.filePath != null && { filePath: s.filePath },
|
|
3493
3558
|
...s.sessionName != null && { sessionName: s.sessionName },
|
|
3494
|
-
...s.firstMessageText != null && { firstMessageText: s.firstMessageText }
|
|
3559
|
+
...s.firstMessageText != null && { firstMessageText: s.firstMessageText },
|
|
3560
|
+
// Unconditional: this shape crosses the pty-host boundary, and a streamer
|
|
3561
|
+
// re-adopting a surviving host's sessions mid-turn has no other source for
|
|
3562
|
+
// the phase — no snapshot or replay event carries it, and setPhase's change
|
|
3563
|
+
// guard means the host will never re-emit it for the rest of that turn.
|
|
3564
|
+
subStatus: s.subStatus ?? null
|
|
3495
3565
|
};
|
|
3496
3566
|
}
|
|
3497
3567
|
|
|
@@ -7955,6 +8025,12 @@ function conversationToResumableSession(c) {
|
|
|
7955
8025
|
branch: c.branch ?? void 0,
|
|
7956
8026
|
lastOutput: "",
|
|
7957
8027
|
elapsedMs: 0,
|
|
8028
|
+
// No PTY behind a cached conversation, so there is no phase — emitted
|
|
8029
|
+
// explicitly for the same reason as in managedToResponse/discoveredToResponse:
|
|
8030
|
+
// the client merges session frames, so an absent key keeps the previous
|
|
8031
|
+
// value and the indicator latches. `GET /api/sessions/:id` serves this
|
|
8032
|
+
// shape whenever the id is a conversation rather than a live session.
|
|
8033
|
+
subStatus: null,
|
|
7958
8034
|
promptCount: c.messageCount,
|
|
7959
8035
|
startedAt: c.lastActivity,
|
|
7960
8036
|
completedAt: null,
|
|
@@ -11911,6 +11987,15 @@ function createLiveSessionOptions(deps) {
|
|
|
11911
11987
|
seq
|
|
11912
11988
|
});
|
|
11913
11989
|
},
|
|
11990
|
+
onPhaseChange: (sessionId, phase) => {
|
|
11991
|
+
deps.sessionStore.updateManaged(sessionId, { subStatus: phase });
|
|
11992
|
+
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
|
|
11993
|
+
type: "session_phase",
|
|
11994
|
+
sessionId,
|
|
11995
|
+
phase,
|
|
11996
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11997
|
+
});
|
|
11998
|
+
},
|
|
11914
11999
|
onUserMessage: (sessionId, text, ts) => {
|
|
11915
12000
|
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
|
|
11916
12001
|
type: "user_message",
|
|
@@ -13940,6 +14025,7 @@ var SessionStore = class {
|
|
|
13940
14025
|
const session = this.managed.get(sessionId);
|
|
13941
14026
|
if (!session) return null;
|
|
13942
14027
|
Object.assign(session, updates);
|
|
14028
|
+
if (updates.status != null && updates.status !== "running") session.subStatus = null;
|
|
13943
14029
|
return session;
|
|
13944
14030
|
}
|
|
13945
14031
|
removeManaged(sessionId) {
|
|
@@ -14120,6 +14206,13 @@ function managedToResponse(s, ptyAttached) {
|
|
|
14120
14206
|
promptCount: s.promptCount,
|
|
14121
14207
|
startedAt: s.startedAt.toISOString(),
|
|
14122
14208
|
completedAt: s.completedAt?.toISOString() ?? null,
|
|
14209
|
+
// Unconditional, like completedAt above — do NOT move this into the
|
|
14210
|
+
// `...(x != null && { x })` block below. That guard uses loose `!=`, which
|
|
14211
|
+
// catches null as well as undefined, and would turn an explicit "no phase"
|
|
14212
|
+
// back into an absent key. The client merges session frames, so an absent
|
|
14213
|
+
// key keeps the previous value and the indicator latches on a finished
|
|
14214
|
+
// turn — the tb-mobile PR #647 bug, arriving through the serialiser.
|
|
14215
|
+
subStatus: s.subStatus ?? null,
|
|
14123
14216
|
ptyAttached,
|
|
14124
14217
|
...s.projectId != null && { projectId: s.projectId },
|
|
14125
14218
|
...s.sessionName != null && { sessionName: s.sessionName },
|
|
@@ -14170,6 +14263,10 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14170
14263
|
// cannot see the process's prompt state.
|
|
14171
14264
|
lifecycle: "detached",
|
|
14172
14265
|
lifecycleSource: "probe",
|
|
14266
|
+
// No PTY here, so nothing to scrape and no phase to report. Emitted
|
|
14267
|
+
// explicitly rather than omitted, for the same reason as in
|
|
14268
|
+
// managedToResponse: absence must never be a third state on the wire.
|
|
14269
|
+
subStatus: null,
|
|
14173
14270
|
projectPath: d.projectPath,
|
|
14174
14271
|
projectName: d.projectName,
|
|
14175
14272
|
branch: d.branch,
|