@threadbase-sh/streamer 1.53.1 → 1.54.1
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 +116 -8
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +105 -7
- 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 +105 -7
- 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
|
|
|
@@ -3949,14 +4019,15 @@ var FULL_CAPABILITIES = [
|
|
|
3949
4019
|
"session:control",
|
|
3950
4020
|
"fs:browse",
|
|
3951
4021
|
"fs:upload",
|
|
3952
|
-
"notifications"
|
|
4022
|
+
"notifications",
|
|
4023
|
+
"admin"
|
|
3953
4024
|
];
|
|
3954
4025
|
var READ_ONLY_CAPABILITIES = ["history:read"];
|
|
3955
4026
|
function capabilitiesForPreset(preset) {
|
|
3956
4027
|
return preset === "read-only" ? [...READ_ONLY_CAPABILITIES] : [...FULL_CAPABILITIES];
|
|
3957
4028
|
}
|
|
3958
4029
|
function legacyPrincipal() {
|
|
3959
|
-
return { kind: "legacy", capabilities: [...FULL_CAPABILITIES
|
|
4030
|
+
return { kind: "legacy", capabilities: [...FULL_CAPABILITIES] };
|
|
3960
4031
|
}
|
|
3961
4032
|
function hasCapability(principal, required) {
|
|
3962
4033
|
return principal.capabilities.includes(required);
|
|
@@ -7955,6 +8026,12 @@ function conversationToResumableSession(c) {
|
|
|
7955
8026
|
branch: c.branch ?? void 0,
|
|
7956
8027
|
lastOutput: "",
|
|
7957
8028
|
elapsedMs: 0,
|
|
8029
|
+
// No PTY behind a cached conversation, so there is no phase — emitted
|
|
8030
|
+
// explicitly for the same reason as in managedToResponse/discoveredToResponse:
|
|
8031
|
+
// the client merges session frames, so an absent key keeps the previous
|
|
8032
|
+
// value and the indicator latches. `GET /api/sessions/:id` serves this
|
|
8033
|
+
// shape whenever the id is a conversation rather than a live session.
|
|
8034
|
+
subStatus: null,
|
|
7958
8035
|
promptCount: c.messageCount,
|
|
7959
8036
|
startedAt: c.lastActivity,
|
|
7960
8037
|
completedAt: null,
|
|
@@ -11911,6 +11988,15 @@ function createLiveSessionOptions(deps) {
|
|
|
11911
11988
|
seq
|
|
11912
11989
|
});
|
|
11913
11990
|
},
|
|
11991
|
+
onPhaseChange: (sessionId, phase) => {
|
|
11992
|
+
deps.sessionStore.updateManaged(sessionId, { subStatus: phase });
|
|
11993
|
+
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
|
|
11994
|
+
type: "session_phase",
|
|
11995
|
+
sessionId,
|
|
11996
|
+
phase,
|
|
11997
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11998
|
+
});
|
|
11999
|
+
},
|
|
11914
12000
|
onUserMessage: (sessionId, text, ts) => {
|
|
11915
12001
|
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
|
|
11916
12002
|
type: "user_message",
|
|
@@ -13940,6 +14026,7 @@ var SessionStore = class {
|
|
|
13940
14026
|
const session = this.managed.get(sessionId);
|
|
13941
14027
|
if (!session) return null;
|
|
13942
14028
|
Object.assign(session, updates);
|
|
14029
|
+
if (updates.status != null && updates.status !== "running") session.subStatus = null;
|
|
13943
14030
|
return session;
|
|
13944
14031
|
}
|
|
13945
14032
|
removeManaged(sessionId) {
|
|
@@ -14120,6 +14207,13 @@ function managedToResponse(s, ptyAttached) {
|
|
|
14120
14207
|
promptCount: s.promptCount,
|
|
14121
14208
|
startedAt: s.startedAt.toISOString(),
|
|
14122
14209
|
completedAt: s.completedAt?.toISOString() ?? null,
|
|
14210
|
+
// Unconditional, like completedAt above — do NOT move this into the
|
|
14211
|
+
// `...(x != null && { x })` block below. That guard uses loose `!=`, which
|
|
14212
|
+
// catches null as well as undefined, and would turn an explicit "no phase"
|
|
14213
|
+
// back into an absent key. The client merges session frames, so an absent
|
|
14214
|
+
// key keeps the previous value and the indicator latches on a finished
|
|
14215
|
+
// turn — the tb-mobile PR #647 bug, arriving through the serialiser.
|
|
14216
|
+
subStatus: s.subStatus ?? null,
|
|
14123
14217
|
ptyAttached,
|
|
14124
14218
|
...s.projectId != null && { projectId: s.projectId },
|
|
14125
14219
|
...s.sessionName != null && { sessionName: s.sessionName },
|
|
@@ -14170,6 +14264,10 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14170
14264
|
// cannot see the process's prompt state.
|
|
14171
14265
|
lifecycle: "detached",
|
|
14172
14266
|
lifecycleSource: "probe",
|
|
14267
|
+
// No PTY here, so nothing to scrape and no phase to report. Emitted
|
|
14268
|
+
// explicitly rather than omitted, for the same reason as in
|
|
14269
|
+
// managedToResponse: absence must never be a third state on the wire.
|
|
14270
|
+
subStatus: null,
|
|
14173
14271
|
projectPath: d.projectPath,
|
|
14174
14272
|
projectName: d.projectName,
|
|
14175
14273
|
branch: d.branch,
|