@threadbase-sh/streamer 1.61.1 → 1.62.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/README.md +8 -0
- package/dist/cli.cjs +129 -33
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +95 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -32
- package/dist/index.d.ts +59 -32
- package/dist/index.js +95 -29
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -497,40 +497,47 @@ function getLogger(component) {
|
|
|
497
497
|
var logger = build(baseLogger);
|
|
498
498
|
|
|
499
499
|
// src/feature-flags.ts
|
|
500
|
-
var FEATURE_FLAGS =
|
|
501
|
-
{
|
|
502
|
-
id: "codexSystemPrompt",
|
|
500
|
+
var FEATURE_FLAGS = {
|
|
501
|
+
codexSystemPrompt: {
|
|
503
502
|
description: "Send the built system prompt to fresh Codex sessions. Off by default: Codex has no --system-prompt flag, so the prompt goes in the positional [PROMPT] argument, which Codex treats as the user's opening turn rather than a system-level instruction.",
|
|
504
503
|
default: false,
|
|
505
504
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
506
505
|
},
|
|
507
|
-
{
|
|
508
|
-
id: "sessionRehydration",
|
|
506
|
+
sessionRehydration: {
|
|
509
507
|
description: "Seed the session list at boot with sessions a previous streamer run left behind, so a restart leaves them one tap from resuming instead of silently gone. On by default, with a kill switch: it changes what GET /api/sessions contains.",
|
|
510
508
|
default: true,
|
|
511
509
|
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
512
510
|
},
|
|
513
|
-
{
|
|
514
|
-
id: "liveActivityPush",
|
|
511
|
+
liveActivityPush: {
|
|
515
512
|
description: "Drive iOS Live Activity surfaces for running sessions. Off by default: the streamer half needs an APNs p8 and a registered push-to-start token, and without both, mobile falls back to starting the activity locally \u2014 which freezes the moment the app backgrounds and expires silently after ~8h. Mobile reads this flag and skips its local path too, so one switch turns the whole surface off.",
|
|
516
513
|
default: false,
|
|
517
514
|
env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH"
|
|
518
515
|
},
|
|
519
|
-
{
|
|
520
|
-
id: "e2ee",
|
|
516
|
+
e2ee: {
|
|
521
517
|
description: "Application-layer encryption between a paired device and this server, independent of TLS, so a tunnel or a LAN observer on the path carries ciphertext. Off by default: it is negotiated per device, never forced, because released mobile builds cannot be force-updated and a server that demanded it would break every one of them.",
|
|
522
518
|
default: false,
|
|
523
519
|
env: "THREADBASE_FEATURE_E2EE"
|
|
524
520
|
},
|
|
525
|
-
{
|
|
526
|
-
id: "ptyHost",
|
|
521
|
+
ptyHost: {
|
|
527
522
|
description: "Keep live PTYs in a separate host process so a streamer restart can reconnect without restarting the agents. Off by default until cross-platform behavior is qualified.",
|
|
528
523
|
default: false,
|
|
529
524
|
env: "THREADBASE_FEATURE_PTY_HOST"
|
|
530
525
|
}
|
|
531
|
-
|
|
526
|
+
};
|
|
527
|
+
var FEATURE_FLAG_IDS = Object.keys(FEATURE_FLAGS);
|
|
528
|
+
var FEATURE_FLAG_LIST = FEATURE_FLAG_IDS.map((id) => ({
|
|
529
|
+
id,
|
|
530
|
+
...FEATURE_FLAGS[id]
|
|
531
|
+
}));
|
|
532
|
+
function isFeatureFlagId(id) {
|
|
533
|
+
return Object.hasOwn(FEATURE_FLAGS, id);
|
|
534
|
+
}
|
|
535
|
+
function getFeatureFlag(id) {
|
|
536
|
+
return { id, ...FEATURE_FLAGS[id] };
|
|
537
|
+
}
|
|
532
538
|
function findFeatureFlag(id) {
|
|
533
|
-
|
|
539
|
+
if (!isFeatureFlagId(id)) return void 0;
|
|
540
|
+
return getFeatureFlag(id);
|
|
534
541
|
}
|
|
535
542
|
function parseBooleanEnv(raw) {
|
|
536
543
|
if (raw === void 0) return void 0;
|
|
@@ -552,10 +559,11 @@ function validateFeatureFlagValues(raw) {
|
|
|
552
559
|
}
|
|
553
560
|
if (dropped.length > 0) {
|
|
554
561
|
getLogger("feature-flags").warn(
|
|
555
|
-
`Ignoring unknown or non-boolean feature flags: ${dropped.join(", ")}`,
|
|
562
|
+
`Ignoring unknown or non-boolean feature flags: ${dropped.join(", ")}. Known ids (FEATURE_FLAGS keys, not env names): ${FEATURE_FLAG_IDS.join(", ")}`,
|
|
556
563
|
{
|
|
557
564
|
event: "config.feature_flags_dropped",
|
|
558
|
-
dropped
|
|
565
|
+
dropped,
|
|
566
|
+
known: FEATURE_FLAG_IDS
|
|
559
567
|
}
|
|
560
568
|
);
|
|
561
569
|
}
|
|
@@ -565,7 +573,7 @@ function resolveFeatureFlags(opts) {
|
|
|
565
573
|
const env = opts?.env ?? process.env;
|
|
566
574
|
const values = {};
|
|
567
575
|
const sources = {};
|
|
568
|
-
for (const def of
|
|
576
|
+
for (const def of FEATURE_FLAG_LIST) {
|
|
569
577
|
const rungs = [
|
|
570
578
|
["override", opts?.override?.[def.id]],
|
|
571
579
|
["env", parseBooleanEnv(env[def.env])],
|
|
@@ -579,10 +587,10 @@ function resolveFeatureFlags(opts) {
|
|
|
579
587
|
return { values, sources };
|
|
580
588
|
}
|
|
581
589
|
function nonDefaultFeatureFlags(values) {
|
|
582
|
-
return
|
|
590
|
+
return FEATURE_FLAG_LIST.filter((f) => values[f.id] !== f.default).map((f) => f.id);
|
|
583
591
|
}
|
|
584
592
|
function describeFeatureFlags(resolution) {
|
|
585
|
-
return
|
|
593
|
+
return FEATURE_FLAG_LIST.map(
|
|
586
594
|
(f) => `${f.id}=${resolution.values[f.id]}(${resolution.sources[f.id]})`
|
|
587
595
|
).join(" ");
|
|
588
596
|
}
|
|
@@ -9142,7 +9150,10 @@ var ConversationHandlers = class {
|
|
|
9142
9150
|
score: r.score,
|
|
9143
9151
|
matches: Array.isArray(r.matches) ? r.matches.map((m) => ({
|
|
9144
9152
|
field: m.field,
|
|
9145
|
-
snippet: m.snippet
|
|
9153
|
+
snippet: m.snippet,
|
|
9154
|
+
// Offsets into `snippet` for the matched tokens. Absent on metadata
|
|
9155
|
+
// hits, and on scanners older than the one that added them.
|
|
9156
|
+
highlights: m.highlights
|
|
9146
9157
|
})) : []
|
|
9147
9158
|
}));
|
|
9148
9159
|
const page = paginate(applyFilters(adapted, filters), offset, limit);
|
|
@@ -10570,7 +10581,9 @@ var SessionHandlers = class {
|
|
|
10570
10581
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
10571
10582
|
return;
|
|
10572
10583
|
}
|
|
10584
|
+
const shouldForget = this.shouldForgetEmptySession(session);
|
|
10573
10585
|
if (session.status === "idle") {
|
|
10586
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
10574
10587
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
10575
10588
|
res.end(JSON.stringify({ status: "already_idle", sessionId }));
|
|
10576
10589
|
return;
|
|
@@ -10598,6 +10611,7 @@ var SessionHandlers = class {
|
|
|
10598
10611
|
this.ptyManager.putOnHold(sessionId);
|
|
10599
10612
|
this.discoveryCache = null;
|
|
10600
10613
|
const outcome = await Promise.race([idlePromise, timeoutPromise]);
|
|
10614
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
10601
10615
|
if (outcome === "idle") {
|
|
10602
10616
|
res.write(`${JSON.stringify({ event: "stopped", sessionId })}
|
|
10603
10617
|
`);
|
|
@@ -10610,6 +10624,42 @@ var SessionHandlers = class {
|
|
|
10610
10624
|
}
|
|
10611
10625
|
res.end();
|
|
10612
10626
|
}
|
|
10627
|
+
/**
|
|
10628
|
+
* An unused start: the user never submitted a prompt, and the conversation
|
|
10629
|
+
* cache has no row for this id (empty Codex/Claude often never write a JSONL).
|
|
10630
|
+
* `conversationId === sessionId` is not evidence of history — only the cache
|
|
10631
|
+
* is. promptCount > 0 or a cache hit keeps today's hold path.
|
|
10632
|
+
*/
|
|
10633
|
+
shouldForgetEmptySession(session) {
|
|
10634
|
+
const stored = this.sessionStore.getManaged(session.id);
|
|
10635
|
+
const promptCount = Math.max(session.promptCount, stored?.promptCount ?? 0);
|
|
10636
|
+
if (promptCount > 0) return false;
|
|
10637
|
+
return !this.hasCachedConversationFor(session, stored);
|
|
10638
|
+
}
|
|
10639
|
+
hasCachedConversationFor(session, stored) {
|
|
10640
|
+
const cache = this.cache;
|
|
10641
|
+
if (!cache) return false;
|
|
10642
|
+
const ids = /* @__PURE__ */ new Set([session.id]);
|
|
10643
|
+
if (session.boundConversationId) ids.add(session.boundConversationId);
|
|
10644
|
+
if (session.resumedFromConversationId) ids.add(session.resumedFromConversationId);
|
|
10645
|
+
if (stored?.boundConversationId) ids.add(stored.boundConversationId);
|
|
10646
|
+
if (stored?.resumedFromConversationId) ids.add(stored.resumedFromConversationId);
|
|
10647
|
+
for (const id of ids) {
|
|
10648
|
+
if (cache.hasConversation(id)) return true;
|
|
10649
|
+
}
|
|
10650
|
+
return false;
|
|
10651
|
+
}
|
|
10652
|
+
forgetEmptyStoppedSession(sessionId) {
|
|
10653
|
+
this.log.info(`[stop] forgetting empty session ${sessionId.slice(0, 8)}`, {
|
|
10654
|
+
event: "session.forget_empty",
|
|
10655
|
+
sessionId
|
|
10656
|
+
});
|
|
10657
|
+
this.deps.forgetSession(sessionId);
|
|
10658
|
+
this.wsHub.broadcast({
|
|
10659
|
+
type: "session_list",
|
|
10660
|
+
sessions: this.sessionStore.list(this.deps.ptyAttachedIds())
|
|
10661
|
+
});
|
|
10662
|
+
}
|
|
10613
10663
|
async handleAdopt(sessionId, res) {
|
|
10614
10664
|
const discovered = await discoverClaudeProcesses();
|
|
10615
10665
|
this.sessionStore.setDiscovered(discovered);
|
|
@@ -15791,6 +15841,7 @@ var StreamerServer = class {
|
|
|
15791
15841
|
spawnFlagOverrides: () => this.spawnFlagOverrides(),
|
|
15792
15842
|
resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId),
|
|
15793
15843
|
waitForStartupOutcome: (sessionId, timeoutMs) => this.waitForStartupOutcome(sessionId, timeoutMs),
|
|
15844
|
+
forgetSession: (sessionId) => this.forgetSession(sessionId),
|
|
15794
15845
|
abandonFailedStart: (sessionId) => this.abandonFailedStart(sessionId),
|
|
15795
15846
|
enrichResumedSessionAsync: (sessionId, projectPath, conv) => this.enrichResumedSessionAsync(sessionId, projectPath, conv),
|
|
15796
15847
|
findJsonlPath: (uuid) => this.conversationHandlers.findJsonlPath(uuid),
|
|
@@ -16782,7 +16833,7 @@ var StreamerServer = class {
|
|
|
16782
16833
|
*/
|
|
16783
16834
|
getFeatureFlagsConfig() {
|
|
16784
16835
|
return {
|
|
16785
|
-
registry:
|
|
16836
|
+
registry: FEATURE_FLAG_LIST,
|
|
16786
16837
|
values: this.featureFlags,
|
|
16787
16838
|
sources: this.featureFlagSources
|
|
16788
16839
|
};
|
|
@@ -17016,29 +17067,44 @@ var StreamerServer = class {
|
|
|
17016
17067
|
});
|
|
17017
17068
|
}
|
|
17018
17069
|
/**
|
|
17019
|
-
* Drop every trace of a session
|
|
17020
|
-
*
|
|
17070
|
+
* Drop every trace of a managed session: in-memory store, durable registry
|
|
17071
|
+
* row, and the collision-probe markers that would otherwise outlive it.
|
|
17021
17072
|
*
|
|
17022
|
-
*
|
|
17023
|
-
*
|
|
17024
|
-
*
|
|
17025
|
-
*
|
|
17026
|
-
*
|
|
17073
|
+
* Used when a start never became usable (`abandonFailedStart`) and when stop
|
|
17074
|
+
* is asked to discard an empty session that has no cached conversation. The
|
|
17075
|
+
* registry delete is load-bearing — `rehydrateSessions` will bring the row
|
|
17076
|
+
* back on the next boot if it remains.
|
|
17077
|
+
*
|
|
17078
|
+
* Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange
|
|
17079
|
+
* on idle writes `selfPtyEndedAt` and a registry status, and those have to
|
|
17080
|
+
* be cleared here afterwards.
|
|
17027
17081
|
*/
|
|
17028
|
-
|
|
17082
|
+
forgetSession(sessionId) {
|
|
17029
17083
|
this.sessionStore.removeManaged(sessionId);
|
|
17030
17084
|
this.selfPtyEndedAt.delete(sessionId);
|
|
17031
17085
|
this.contendedSessions.delete(sessionId);
|
|
17032
17086
|
try {
|
|
17033
17087
|
this.managedSessionsRepo?.delete(sessionId);
|
|
17034
17088
|
} catch (err) {
|
|
17035
|
-
this.log.warn("[registry] failed to drop a
|
|
17089
|
+
this.log.warn("[registry] failed to drop a session", {
|
|
17036
17090
|
event: "registry.forget_failed",
|
|
17037
17091
|
sessionId,
|
|
17038
17092
|
err
|
|
17039
17093
|
});
|
|
17040
17094
|
}
|
|
17041
17095
|
}
|
|
17096
|
+
/**
|
|
17097
|
+
* Drop every trace of a session that never became usable.
|
|
17098
|
+
*
|
|
17099
|
+
* The runner has already torn itself down (failStartup / handleExit); what
|
|
17100
|
+
* remains is server-side bookkeeping that would otherwise leave a dead
|
|
17101
|
+
* session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt`
|
|
17102
|
+
* marker that would suppress the mtime collision signal on the NEXT resume —
|
|
17103
|
+
* i.e. it would help hide the very owner we just collided with.
|
|
17104
|
+
*/
|
|
17105
|
+
abandonFailedStart(sessionId) {
|
|
17106
|
+
this.forgetSession(sessionId);
|
|
17107
|
+
}
|
|
17042
17108
|
enrichResumedSessionAsync(sessionId, projectPath, conv) {
|
|
17043
17109
|
try {
|
|
17044
17110
|
if (!this.sessionStore.getManaged(sessionId)) return;
|