@threadbase-sh/streamer 1.61.0 → 1.61.2
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 +604 -134
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +101 -35
- 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 +101 -35
- 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
|
}
|
|
@@ -10570,7 +10578,9 @@ var SessionHandlers = class {
|
|
|
10570
10578
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
10571
10579
|
return;
|
|
10572
10580
|
}
|
|
10581
|
+
const shouldForget = this.shouldForgetEmptySession(session);
|
|
10573
10582
|
if (session.status === "idle") {
|
|
10583
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
10574
10584
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
10575
10585
|
res.end(JSON.stringify({ status: "already_idle", sessionId }));
|
|
10576
10586
|
return;
|
|
@@ -10598,6 +10608,7 @@ var SessionHandlers = class {
|
|
|
10598
10608
|
this.ptyManager.putOnHold(sessionId);
|
|
10599
10609
|
this.discoveryCache = null;
|
|
10600
10610
|
const outcome = await Promise.race([idlePromise, timeoutPromise]);
|
|
10611
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
10601
10612
|
if (outcome === "idle") {
|
|
10602
10613
|
res.write(`${JSON.stringify({ event: "stopped", sessionId })}
|
|
10603
10614
|
`);
|
|
@@ -10610,6 +10621,42 @@ var SessionHandlers = class {
|
|
|
10610
10621
|
}
|
|
10611
10622
|
res.end();
|
|
10612
10623
|
}
|
|
10624
|
+
/**
|
|
10625
|
+
* An unused start: the user never submitted a prompt, and the conversation
|
|
10626
|
+
* cache has no row for this id (empty Codex/Claude often never write a JSONL).
|
|
10627
|
+
* `conversationId === sessionId` is not evidence of history — only the cache
|
|
10628
|
+
* is. promptCount > 0 or a cache hit keeps today's hold path.
|
|
10629
|
+
*/
|
|
10630
|
+
shouldForgetEmptySession(session) {
|
|
10631
|
+
const stored = this.sessionStore.getManaged(session.id);
|
|
10632
|
+
const promptCount = Math.max(session.promptCount, stored?.promptCount ?? 0);
|
|
10633
|
+
if (promptCount > 0) return false;
|
|
10634
|
+
return !this.hasCachedConversationFor(session, stored);
|
|
10635
|
+
}
|
|
10636
|
+
hasCachedConversationFor(session, stored) {
|
|
10637
|
+
const cache = this.cache;
|
|
10638
|
+
if (!cache) return false;
|
|
10639
|
+
const ids = /* @__PURE__ */ new Set([session.id]);
|
|
10640
|
+
if (session.boundConversationId) ids.add(session.boundConversationId);
|
|
10641
|
+
if (session.resumedFromConversationId) ids.add(session.resumedFromConversationId);
|
|
10642
|
+
if (stored?.boundConversationId) ids.add(stored.boundConversationId);
|
|
10643
|
+
if (stored?.resumedFromConversationId) ids.add(stored.resumedFromConversationId);
|
|
10644
|
+
for (const id of ids) {
|
|
10645
|
+
if (cache.hasConversation(id)) return true;
|
|
10646
|
+
}
|
|
10647
|
+
return false;
|
|
10648
|
+
}
|
|
10649
|
+
forgetEmptyStoppedSession(sessionId) {
|
|
10650
|
+
this.log.info(`[stop] forgetting empty session ${sessionId.slice(0, 8)}`, {
|
|
10651
|
+
event: "session.forget_empty",
|
|
10652
|
+
sessionId
|
|
10653
|
+
});
|
|
10654
|
+
this.deps.forgetSession(sessionId);
|
|
10655
|
+
this.wsHub.broadcast({
|
|
10656
|
+
type: "session_list",
|
|
10657
|
+
sessions: this.sessionStore.list(this.deps.ptyAttachedIds())
|
|
10658
|
+
});
|
|
10659
|
+
}
|
|
10613
10660
|
async handleAdopt(sessionId, res) {
|
|
10614
10661
|
const discovered = await discoverClaudeProcesses();
|
|
10615
10662
|
this.sessionStore.setDiscovered(discovered);
|
|
@@ -15791,6 +15838,7 @@ var StreamerServer = class {
|
|
|
15791
15838
|
spawnFlagOverrides: () => this.spawnFlagOverrides(),
|
|
15792
15839
|
resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId),
|
|
15793
15840
|
waitForStartupOutcome: (sessionId, timeoutMs) => this.waitForStartupOutcome(sessionId, timeoutMs),
|
|
15841
|
+
forgetSession: (sessionId) => this.forgetSession(sessionId),
|
|
15794
15842
|
abandonFailedStart: (sessionId) => this.abandonFailedStart(sessionId),
|
|
15795
15843
|
enrichResumedSessionAsync: (sessionId, projectPath, conv) => this.enrichResumedSessionAsync(sessionId, projectPath, conv),
|
|
15796
15844
|
findJsonlPath: (uuid) => this.conversationHandlers.findJsonlPath(uuid),
|
|
@@ -16613,13 +16661,16 @@ var StreamerServer = class {
|
|
|
16613
16661
|
json(res, 400, { error: "Missing token or clientPublicKey" });
|
|
16614
16662
|
return;
|
|
16615
16663
|
}
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16664
|
+
const e2eeEnabled = describeE2eeCapability(this.featureFlags.e2ee).enabled;
|
|
16665
|
+
let e2eeRequest = null;
|
|
16666
|
+
if (e2eeEnabled) {
|
|
16667
|
+
try {
|
|
16668
|
+
e2eeRequest = parseE2eeRequest(body?.e2ee);
|
|
16669
|
+
} catch (err) {
|
|
16670
|
+
const e = err;
|
|
16671
|
+
json(res, 400, { error: e.message, code: e.code });
|
|
16672
|
+
return;
|
|
16673
|
+
}
|
|
16623
16674
|
}
|
|
16624
16675
|
const precheck = this.pairTokens.wouldConsume(token);
|
|
16625
16676
|
if (!precheck.ok) {
|
|
@@ -16779,7 +16830,7 @@ var StreamerServer = class {
|
|
|
16779
16830
|
*/
|
|
16780
16831
|
getFeatureFlagsConfig() {
|
|
16781
16832
|
return {
|
|
16782
|
-
registry:
|
|
16833
|
+
registry: FEATURE_FLAG_LIST,
|
|
16783
16834
|
values: this.featureFlags,
|
|
16784
16835
|
sources: this.featureFlagSources
|
|
16785
16836
|
};
|
|
@@ -17013,29 +17064,44 @@ var StreamerServer = class {
|
|
|
17013
17064
|
});
|
|
17014
17065
|
}
|
|
17015
17066
|
/**
|
|
17016
|
-
* Drop every trace of a session
|
|
17017
|
-
*
|
|
17067
|
+
* Drop every trace of a managed session: in-memory store, durable registry
|
|
17068
|
+
* row, and the collision-probe markers that would otherwise outlive it.
|
|
17018
17069
|
*
|
|
17019
|
-
*
|
|
17020
|
-
*
|
|
17021
|
-
*
|
|
17022
|
-
*
|
|
17023
|
-
*
|
|
17070
|
+
* Used when a start never became usable (`abandonFailedStart`) and when stop
|
|
17071
|
+
* is asked to discard an empty session that has no cached conversation. The
|
|
17072
|
+
* registry delete is load-bearing — `rehydrateSessions` will bring the row
|
|
17073
|
+
* back on the next boot if it remains.
|
|
17074
|
+
*
|
|
17075
|
+
* Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange
|
|
17076
|
+
* on idle writes `selfPtyEndedAt` and a registry status, and those have to
|
|
17077
|
+
* be cleared here afterwards.
|
|
17024
17078
|
*/
|
|
17025
|
-
|
|
17079
|
+
forgetSession(sessionId) {
|
|
17026
17080
|
this.sessionStore.removeManaged(sessionId);
|
|
17027
17081
|
this.selfPtyEndedAt.delete(sessionId);
|
|
17028
17082
|
this.contendedSessions.delete(sessionId);
|
|
17029
17083
|
try {
|
|
17030
17084
|
this.managedSessionsRepo?.delete(sessionId);
|
|
17031
17085
|
} catch (err) {
|
|
17032
|
-
this.log.warn("[registry] failed to drop a
|
|
17086
|
+
this.log.warn("[registry] failed to drop a session", {
|
|
17033
17087
|
event: "registry.forget_failed",
|
|
17034
17088
|
sessionId,
|
|
17035
17089
|
err
|
|
17036
17090
|
});
|
|
17037
17091
|
}
|
|
17038
17092
|
}
|
|
17093
|
+
/**
|
|
17094
|
+
* Drop every trace of a session that never became usable.
|
|
17095
|
+
*
|
|
17096
|
+
* The runner has already torn itself down (failStartup / handleExit); what
|
|
17097
|
+
* remains is server-side bookkeeping that would otherwise leave a dead
|
|
17098
|
+
* session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt`
|
|
17099
|
+
* marker that would suppress the mtime collision signal on the NEXT resume —
|
|
17100
|
+
* i.e. it would help hide the very owner we just collided with.
|
|
17101
|
+
*/
|
|
17102
|
+
abandonFailedStart(sessionId) {
|
|
17103
|
+
this.forgetSession(sessionId);
|
|
17104
|
+
}
|
|
17039
17105
|
enrichResumedSessionAsync(sessionId, projectPath, conv) {
|
|
17040
17106
|
try {
|
|
17041
17107
|
if (!this.sessionStore.getManaged(sessionId)) return;
|