@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.d.cts
CHANGED
|
@@ -105,15 +105,19 @@ interface FlagDefinition {
|
|
|
105
105
|
type ClaudeFlagValue = string | string[] | boolean;
|
|
106
106
|
type ClaudeFlagValues = Record<string, ClaudeFlagValue>;
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
id: string;
|
|
108
|
+
/** Spec for one flag in the keyed registry. The id is the object key, not a field. */
|
|
109
|
+
interface FeatureFlagSpec {
|
|
111
110
|
/** Shipped to clients alongside the values so a UI can render it. */
|
|
112
111
|
description: string;
|
|
113
112
|
default: boolean;
|
|
114
113
|
/** Full env var name. */
|
|
115
114
|
env: string;
|
|
116
115
|
}
|
|
116
|
+
/** Wire shape: the keyed spec plus its id, for GET /api/config/feature-flags. */
|
|
117
|
+
interface FeatureFlagDefinition extends FeatureFlagSpec {
|
|
118
|
+
/** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
|
|
119
|
+
id: FeatureFlagId;
|
|
120
|
+
}
|
|
117
121
|
/**
|
|
118
122
|
* Values arriving from a config source. PARTIAL on purpose: server.yaml, the
|
|
119
123
|
* CLI and ServerConfig each speak about the flags they mention and stay silent
|
|
@@ -130,32 +134,42 @@ type ResolvedFeatureFlags = Record<FeatureFlagId, boolean>;
|
|
|
130
134
|
* `override` is the legacy ServerConfig field (see resolveFeatureFlags).
|
|
131
135
|
*/
|
|
132
136
|
type FeatureFlagSource = "override" | "env" | "cli" | "yaml" | "default";
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The registry, keyed by flag name.
|
|
139
|
+
*
|
|
140
|
+
* Prefer `FEATURE_FLAGS.ptyHost` over a string lookup. A typo is a compile
|
|
141
|
+
* error; `findFeatureFlag("…")` is only for untrusted yaml/CLI tokens.
|
|
142
|
+
*
|
|
143
|
+
* `as const` + `keyof` is what makes `flags.ptyHsot` a compile error instead of
|
|
144
|
+
* `undefined`. A TS enum would add a runtime object without a stronger type.
|
|
145
|
+
*/
|
|
146
|
+
declare const FEATURE_FLAGS: {
|
|
147
|
+
readonly codexSystemPrompt: {
|
|
148
|
+
readonly description: string;
|
|
149
|
+
readonly default: false;
|
|
150
|
+
readonly env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT";
|
|
151
|
+
};
|
|
152
|
+
readonly sessionRehydration: {
|
|
153
|
+
readonly description: string;
|
|
154
|
+
readonly default: true;
|
|
155
|
+
readonly env: "THREADBASE_FEATURE_SESSION_REHYDRATION";
|
|
156
|
+
};
|
|
157
|
+
readonly liveActivityPush: {
|
|
158
|
+
readonly description: string;
|
|
159
|
+
readonly default: false;
|
|
160
|
+
readonly env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH";
|
|
161
|
+
};
|
|
162
|
+
readonly e2ee: {
|
|
163
|
+
readonly description: string;
|
|
164
|
+
readonly default: false;
|
|
165
|
+
readonly env: "THREADBASE_FEATURE_E2EE";
|
|
166
|
+
};
|
|
167
|
+
readonly ptyHost: {
|
|
168
|
+
readonly description: string;
|
|
169
|
+
readonly default: false;
|
|
170
|
+
readonly env: "THREADBASE_FEATURE_PTY_HOST";
|
|
171
|
+
};
|
|
172
|
+
};
|
|
159
173
|
/**
|
|
160
174
|
* The registry's ids as a union, derived rather than declared.
|
|
161
175
|
*
|
|
@@ -165,7 +179,7 @@ declare const FEATURE_FLAGS: readonly [{
|
|
|
165
179
|
* was meant to gate — the exact failure the "total map" contract exists to
|
|
166
180
|
* prevent, reachable through the one door that contract left open.
|
|
167
181
|
*/
|
|
168
|
-
type FeatureFlagId =
|
|
182
|
+
type FeatureFlagId = keyof typeof FEATURE_FLAGS;
|
|
169
183
|
|
|
170
184
|
declare const CLAUDE_CODE_PROVIDER: "claude-code";
|
|
171
185
|
declare const CODEX_CLI_PROVIDER: "codex-cli";
|
|
@@ -2509,8 +2523,21 @@ declare class StreamerServer {
|
|
|
2509
2523
|
*/
|
|
2510
2524
|
private waitForStartupOutcome;
|
|
2511
2525
|
/**
|
|
2512
|
-
* Drop every trace of a session
|
|
2513
|
-
*
|
|
2526
|
+
* Drop every trace of a managed session: in-memory store, durable registry
|
|
2527
|
+
* row, and the collision-probe markers that would otherwise outlive it.
|
|
2528
|
+
*
|
|
2529
|
+
* Used when a start never became usable (`abandonFailedStart`) and when stop
|
|
2530
|
+
* is asked to discard an empty session that has no cached conversation. The
|
|
2531
|
+
* registry delete is load-bearing — `rehydrateSessions` will bring the row
|
|
2532
|
+
* back on the next boot if it remains.
|
|
2533
|
+
*
|
|
2534
|
+
* Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange
|
|
2535
|
+
* on idle writes `selfPtyEndedAt` and a registry status, and those have to
|
|
2536
|
+
* be cleared here afterwards.
|
|
2537
|
+
*/
|
|
2538
|
+
private forgetSession;
|
|
2539
|
+
/**
|
|
2540
|
+
* Drop every trace of a session that never became usable.
|
|
2514
2541
|
*
|
|
2515
2542
|
* The runner has already torn itself down (failStartup / handleExit); what
|
|
2516
2543
|
* remains is server-side bookkeeping that would otherwise leave a dead
|
package/dist/index.d.ts
CHANGED
|
@@ -105,15 +105,19 @@ interface FlagDefinition {
|
|
|
105
105
|
type ClaudeFlagValue = string | string[] | boolean;
|
|
106
106
|
type ClaudeFlagValues = Record<string, ClaudeFlagValue>;
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
id: string;
|
|
108
|
+
/** Spec for one flag in the keyed registry. The id is the object key, not a field. */
|
|
109
|
+
interface FeatureFlagSpec {
|
|
111
110
|
/** Shipped to clients alongside the values so a UI can render it. */
|
|
112
111
|
description: string;
|
|
113
112
|
default: boolean;
|
|
114
113
|
/** Full env var name. */
|
|
115
114
|
env: string;
|
|
116
115
|
}
|
|
116
|
+
/** Wire shape: the keyed spec plus its id, for GET /api/config/feature-flags. */
|
|
117
|
+
interface FeatureFlagDefinition extends FeatureFlagSpec {
|
|
118
|
+
/** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
|
|
119
|
+
id: FeatureFlagId;
|
|
120
|
+
}
|
|
117
121
|
/**
|
|
118
122
|
* Values arriving from a config source. PARTIAL on purpose: server.yaml, the
|
|
119
123
|
* CLI and ServerConfig each speak about the flags they mention and stay silent
|
|
@@ -130,32 +134,42 @@ type ResolvedFeatureFlags = Record<FeatureFlagId, boolean>;
|
|
|
130
134
|
* `override` is the legacy ServerConfig field (see resolveFeatureFlags).
|
|
131
135
|
*/
|
|
132
136
|
type FeatureFlagSource = "override" | "env" | "cli" | "yaml" | "default";
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The registry, keyed by flag name.
|
|
139
|
+
*
|
|
140
|
+
* Prefer `FEATURE_FLAGS.ptyHost` over a string lookup. A typo is a compile
|
|
141
|
+
* error; `findFeatureFlag("…")` is only for untrusted yaml/CLI tokens.
|
|
142
|
+
*
|
|
143
|
+
* `as const` + `keyof` is what makes `flags.ptyHsot` a compile error instead of
|
|
144
|
+
* `undefined`. A TS enum would add a runtime object without a stronger type.
|
|
145
|
+
*/
|
|
146
|
+
declare const FEATURE_FLAGS: {
|
|
147
|
+
readonly codexSystemPrompt: {
|
|
148
|
+
readonly description: string;
|
|
149
|
+
readonly default: false;
|
|
150
|
+
readonly env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT";
|
|
151
|
+
};
|
|
152
|
+
readonly sessionRehydration: {
|
|
153
|
+
readonly description: string;
|
|
154
|
+
readonly default: true;
|
|
155
|
+
readonly env: "THREADBASE_FEATURE_SESSION_REHYDRATION";
|
|
156
|
+
};
|
|
157
|
+
readonly liveActivityPush: {
|
|
158
|
+
readonly description: string;
|
|
159
|
+
readonly default: false;
|
|
160
|
+
readonly env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH";
|
|
161
|
+
};
|
|
162
|
+
readonly e2ee: {
|
|
163
|
+
readonly description: string;
|
|
164
|
+
readonly default: false;
|
|
165
|
+
readonly env: "THREADBASE_FEATURE_E2EE";
|
|
166
|
+
};
|
|
167
|
+
readonly ptyHost: {
|
|
168
|
+
readonly description: string;
|
|
169
|
+
readonly default: false;
|
|
170
|
+
readonly env: "THREADBASE_FEATURE_PTY_HOST";
|
|
171
|
+
};
|
|
172
|
+
};
|
|
159
173
|
/**
|
|
160
174
|
* The registry's ids as a union, derived rather than declared.
|
|
161
175
|
*
|
|
@@ -165,7 +179,7 @@ declare const FEATURE_FLAGS: readonly [{
|
|
|
165
179
|
* was meant to gate — the exact failure the "total map" contract exists to
|
|
166
180
|
* prevent, reachable through the one door that contract left open.
|
|
167
181
|
*/
|
|
168
|
-
type FeatureFlagId =
|
|
182
|
+
type FeatureFlagId = keyof typeof FEATURE_FLAGS;
|
|
169
183
|
|
|
170
184
|
declare const CLAUDE_CODE_PROVIDER: "claude-code";
|
|
171
185
|
declare const CODEX_CLI_PROVIDER: "codex-cli";
|
|
@@ -2509,8 +2523,21 @@ declare class StreamerServer {
|
|
|
2509
2523
|
*/
|
|
2510
2524
|
private waitForStartupOutcome;
|
|
2511
2525
|
/**
|
|
2512
|
-
* Drop every trace of a session
|
|
2513
|
-
*
|
|
2526
|
+
* Drop every trace of a managed session: in-memory store, durable registry
|
|
2527
|
+
* row, and the collision-probe markers that would otherwise outlive it.
|
|
2528
|
+
*
|
|
2529
|
+
* Used when a start never became usable (`abandonFailedStart`) and when stop
|
|
2530
|
+
* is asked to discard an empty session that has no cached conversation. The
|
|
2531
|
+
* registry delete is load-bearing — `rehydrateSessions` will bring the row
|
|
2532
|
+
* back on the next boot if it remains.
|
|
2533
|
+
*
|
|
2534
|
+
* Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange
|
|
2535
|
+
* on idle writes `selfPtyEndedAt` and a registry status, and those have to
|
|
2536
|
+
* be cleared here afterwards.
|
|
2537
|
+
*/
|
|
2538
|
+
private forgetSession;
|
|
2539
|
+
/**
|
|
2540
|
+
* Drop every trace of a session that never became usable.
|
|
2514
2541
|
*
|
|
2515
2542
|
* The runner has already torn itself down (failStartup / handleExit); what
|
|
2516
2543
|
* remains is server-side bookkeeping that would otherwise leave a dead
|
package/dist/index.js
CHANGED
|
@@ -445,40 +445,47 @@ function getLogger(component) {
|
|
|
445
445
|
var logger = build(baseLogger);
|
|
446
446
|
|
|
447
447
|
// src/feature-flags.ts
|
|
448
|
-
var FEATURE_FLAGS =
|
|
449
|
-
{
|
|
450
|
-
id: "codexSystemPrompt",
|
|
448
|
+
var FEATURE_FLAGS = {
|
|
449
|
+
codexSystemPrompt: {
|
|
451
450
|
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.",
|
|
452
451
|
default: false,
|
|
453
452
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
454
453
|
},
|
|
455
|
-
{
|
|
456
|
-
id: "sessionRehydration",
|
|
454
|
+
sessionRehydration: {
|
|
457
455
|
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.",
|
|
458
456
|
default: true,
|
|
459
457
|
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
460
458
|
},
|
|
461
|
-
{
|
|
462
|
-
id: "liveActivityPush",
|
|
459
|
+
liveActivityPush: {
|
|
463
460
|
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.",
|
|
464
461
|
default: false,
|
|
465
462
|
env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH"
|
|
466
463
|
},
|
|
467
|
-
{
|
|
468
|
-
id: "e2ee",
|
|
464
|
+
e2ee: {
|
|
469
465
|
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.",
|
|
470
466
|
default: false,
|
|
471
467
|
env: "THREADBASE_FEATURE_E2EE"
|
|
472
468
|
},
|
|
473
|
-
{
|
|
474
|
-
id: "ptyHost",
|
|
469
|
+
ptyHost: {
|
|
475
470
|
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.",
|
|
476
471
|
default: false,
|
|
477
472
|
env: "THREADBASE_FEATURE_PTY_HOST"
|
|
478
473
|
}
|
|
479
|
-
|
|
474
|
+
};
|
|
475
|
+
var FEATURE_FLAG_IDS = Object.keys(FEATURE_FLAGS);
|
|
476
|
+
var FEATURE_FLAG_LIST = FEATURE_FLAG_IDS.map((id) => ({
|
|
477
|
+
id,
|
|
478
|
+
...FEATURE_FLAGS[id]
|
|
479
|
+
}));
|
|
480
|
+
function isFeatureFlagId(id) {
|
|
481
|
+
return Object.hasOwn(FEATURE_FLAGS, id);
|
|
482
|
+
}
|
|
483
|
+
function getFeatureFlag(id) {
|
|
484
|
+
return { id, ...FEATURE_FLAGS[id] };
|
|
485
|
+
}
|
|
480
486
|
function findFeatureFlag(id) {
|
|
481
|
-
|
|
487
|
+
if (!isFeatureFlagId(id)) return void 0;
|
|
488
|
+
return getFeatureFlag(id);
|
|
482
489
|
}
|
|
483
490
|
function parseBooleanEnv(raw) {
|
|
484
491
|
if (raw === void 0) return void 0;
|
|
@@ -500,10 +507,11 @@ function validateFeatureFlagValues(raw) {
|
|
|
500
507
|
}
|
|
501
508
|
if (dropped.length > 0) {
|
|
502
509
|
getLogger("feature-flags").warn(
|
|
503
|
-
`Ignoring unknown or non-boolean feature flags: ${dropped.join(", ")}`,
|
|
510
|
+
`Ignoring unknown or non-boolean feature flags: ${dropped.join(", ")}. Known ids (FEATURE_FLAGS keys, not env names): ${FEATURE_FLAG_IDS.join(", ")}`,
|
|
504
511
|
{
|
|
505
512
|
event: "config.feature_flags_dropped",
|
|
506
|
-
dropped
|
|
513
|
+
dropped,
|
|
514
|
+
known: FEATURE_FLAG_IDS
|
|
507
515
|
}
|
|
508
516
|
);
|
|
509
517
|
}
|
|
@@ -513,7 +521,7 @@ function resolveFeatureFlags(opts) {
|
|
|
513
521
|
const env = opts?.env ?? process.env;
|
|
514
522
|
const values = {};
|
|
515
523
|
const sources = {};
|
|
516
|
-
for (const def of
|
|
524
|
+
for (const def of FEATURE_FLAG_LIST) {
|
|
517
525
|
const rungs = [
|
|
518
526
|
["override", opts?.override?.[def.id]],
|
|
519
527
|
["env", parseBooleanEnv(env[def.env])],
|
|
@@ -527,10 +535,10 @@ function resolveFeatureFlags(opts) {
|
|
|
527
535
|
return { values, sources };
|
|
528
536
|
}
|
|
529
537
|
function nonDefaultFeatureFlags(values) {
|
|
530
|
-
return
|
|
538
|
+
return FEATURE_FLAG_LIST.filter((f) => values[f.id] !== f.default).map((f) => f.id);
|
|
531
539
|
}
|
|
532
540
|
function describeFeatureFlags(resolution) {
|
|
533
|
-
return
|
|
541
|
+
return FEATURE_FLAG_LIST.map(
|
|
534
542
|
(f) => `${f.id}=${resolution.values[f.id]}(${resolution.sources[f.id]})`
|
|
535
543
|
).join(" ");
|
|
536
544
|
}
|
|
@@ -10530,7 +10538,9 @@ var SessionHandlers = class {
|
|
|
10530
10538
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
10531
10539
|
return;
|
|
10532
10540
|
}
|
|
10541
|
+
const shouldForget = this.shouldForgetEmptySession(session);
|
|
10533
10542
|
if (session.status === "idle") {
|
|
10543
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
10534
10544
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
10535
10545
|
res.end(JSON.stringify({ status: "already_idle", sessionId }));
|
|
10536
10546
|
return;
|
|
@@ -10558,6 +10568,7 @@ var SessionHandlers = class {
|
|
|
10558
10568
|
this.ptyManager.putOnHold(sessionId);
|
|
10559
10569
|
this.discoveryCache = null;
|
|
10560
10570
|
const outcome = await Promise.race([idlePromise, timeoutPromise]);
|
|
10571
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
10561
10572
|
if (outcome === "idle") {
|
|
10562
10573
|
res.write(`${JSON.stringify({ event: "stopped", sessionId })}
|
|
10563
10574
|
`);
|
|
@@ -10570,6 +10581,42 @@ var SessionHandlers = class {
|
|
|
10570
10581
|
}
|
|
10571
10582
|
res.end();
|
|
10572
10583
|
}
|
|
10584
|
+
/**
|
|
10585
|
+
* An unused start: the user never submitted a prompt, and the conversation
|
|
10586
|
+
* cache has no row for this id (empty Codex/Claude often never write a JSONL).
|
|
10587
|
+
* `conversationId === sessionId` is not evidence of history — only the cache
|
|
10588
|
+
* is. promptCount > 0 or a cache hit keeps today's hold path.
|
|
10589
|
+
*/
|
|
10590
|
+
shouldForgetEmptySession(session) {
|
|
10591
|
+
const stored = this.sessionStore.getManaged(session.id);
|
|
10592
|
+
const promptCount = Math.max(session.promptCount, stored?.promptCount ?? 0);
|
|
10593
|
+
if (promptCount > 0) return false;
|
|
10594
|
+
return !this.hasCachedConversationFor(session, stored);
|
|
10595
|
+
}
|
|
10596
|
+
hasCachedConversationFor(session, stored) {
|
|
10597
|
+
const cache = this.cache;
|
|
10598
|
+
if (!cache) return false;
|
|
10599
|
+
const ids = /* @__PURE__ */ new Set([session.id]);
|
|
10600
|
+
if (session.boundConversationId) ids.add(session.boundConversationId);
|
|
10601
|
+
if (session.resumedFromConversationId) ids.add(session.resumedFromConversationId);
|
|
10602
|
+
if (stored?.boundConversationId) ids.add(stored.boundConversationId);
|
|
10603
|
+
if (stored?.resumedFromConversationId) ids.add(stored.resumedFromConversationId);
|
|
10604
|
+
for (const id of ids) {
|
|
10605
|
+
if (cache.hasConversation(id)) return true;
|
|
10606
|
+
}
|
|
10607
|
+
return false;
|
|
10608
|
+
}
|
|
10609
|
+
forgetEmptyStoppedSession(sessionId) {
|
|
10610
|
+
this.log.info(`[stop] forgetting empty session ${sessionId.slice(0, 8)}`, {
|
|
10611
|
+
event: "session.forget_empty",
|
|
10612
|
+
sessionId
|
|
10613
|
+
});
|
|
10614
|
+
this.deps.forgetSession(sessionId);
|
|
10615
|
+
this.wsHub.broadcast({
|
|
10616
|
+
type: "session_list",
|
|
10617
|
+
sessions: this.sessionStore.list(this.deps.ptyAttachedIds())
|
|
10618
|
+
});
|
|
10619
|
+
}
|
|
10573
10620
|
async handleAdopt(sessionId, res) {
|
|
10574
10621
|
const discovered = await discoverClaudeProcesses();
|
|
10575
10622
|
this.sessionStore.setDiscovered(discovered);
|
|
@@ -15762,6 +15809,7 @@ var StreamerServer = class {
|
|
|
15762
15809
|
spawnFlagOverrides: () => this.spawnFlagOverrides(),
|
|
15763
15810
|
resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId),
|
|
15764
15811
|
waitForStartupOutcome: (sessionId, timeoutMs) => this.waitForStartupOutcome(sessionId, timeoutMs),
|
|
15812
|
+
forgetSession: (sessionId) => this.forgetSession(sessionId),
|
|
15765
15813
|
abandonFailedStart: (sessionId) => this.abandonFailedStart(sessionId),
|
|
15766
15814
|
enrichResumedSessionAsync: (sessionId, projectPath, conv) => this.enrichResumedSessionAsync(sessionId, projectPath, conv),
|
|
15767
15815
|
findJsonlPath: (uuid) => this.conversationHandlers.findJsonlPath(uuid),
|
|
@@ -16584,13 +16632,16 @@ var StreamerServer = class {
|
|
|
16584
16632
|
json(res, 400, { error: "Missing token or clientPublicKey" });
|
|
16585
16633
|
return;
|
|
16586
16634
|
}
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16592
|
-
|
|
16593
|
-
|
|
16635
|
+
const e2eeEnabled = describeE2eeCapability(this.featureFlags.e2ee).enabled;
|
|
16636
|
+
let e2eeRequest = null;
|
|
16637
|
+
if (e2eeEnabled) {
|
|
16638
|
+
try {
|
|
16639
|
+
e2eeRequest = parseE2eeRequest(body?.e2ee);
|
|
16640
|
+
} catch (err) {
|
|
16641
|
+
const e = err;
|
|
16642
|
+
json(res, 400, { error: e.message, code: e.code });
|
|
16643
|
+
return;
|
|
16644
|
+
}
|
|
16594
16645
|
}
|
|
16595
16646
|
const precheck = this.pairTokens.wouldConsume(token);
|
|
16596
16647
|
if (!precheck.ok) {
|
|
@@ -16750,7 +16801,7 @@ var StreamerServer = class {
|
|
|
16750
16801
|
*/
|
|
16751
16802
|
getFeatureFlagsConfig() {
|
|
16752
16803
|
return {
|
|
16753
|
-
registry:
|
|
16804
|
+
registry: FEATURE_FLAG_LIST,
|
|
16754
16805
|
values: this.featureFlags,
|
|
16755
16806
|
sources: this.featureFlagSources
|
|
16756
16807
|
};
|
|
@@ -16984,29 +17035,44 @@ var StreamerServer = class {
|
|
|
16984
17035
|
});
|
|
16985
17036
|
}
|
|
16986
17037
|
/**
|
|
16987
|
-
* Drop every trace of a session
|
|
16988
|
-
*
|
|
17038
|
+
* Drop every trace of a managed session: in-memory store, durable registry
|
|
17039
|
+
* row, and the collision-probe markers that would otherwise outlive it.
|
|
16989
17040
|
*
|
|
16990
|
-
*
|
|
16991
|
-
*
|
|
16992
|
-
*
|
|
16993
|
-
*
|
|
16994
|
-
*
|
|
17041
|
+
* Used when a start never became usable (`abandonFailedStart`) and when stop
|
|
17042
|
+
* is asked to discard an empty session that has no cached conversation. The
|
|
17043
|
+
* registry delete is load-bearing — `rehydrateSessions` will bring the row
|
|
17044
|
+
* back on the next boot if it remains.
|
|
17045
|
+
*
|
|
17046
|
+
* Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange
|
|
17047
|
+
* on idle writes `selfPtyEndedAt` and a registry status, and those have to
|
|
17048
|
+
* be cleared here afterwards.
|
|
16995
17049
|
*/
|
|
16996
|
-
|
|
17050
|
+
forgetSession(sessionId) {
|
|
16997
17051
|
this.sessionStore.removeManaged(sessionId);
|
|
16998
17052
|
this.selfPtyEndedAt.delete(sessionId);
|
|
16999
17053
|
this.contendedSessions.delete(sessionId);
|
|
17000
17054
|
try {
|
|
17001
17055
|
this.managedSessionsRepo?.delete(sessionId);
|
|
17002
17056
|
} catch (err) {
|
|
17003
|
-
this.log.warn("[registry] failed to drop a
|
|
17057
|
+
this.log.warn("[registry] failed to drop a session", {
|
|
17004
17058
|
event: "registry.forget_failed",
|
|
17005
17059
|
sessionId,
|
|
17006
17060
|
err
|
|
17007
17061
|
});
|
|
17008
17062
|
}
|
|
17009
17063
|
}
|
|
17064
|
+
/**
|
|
17065
|
+
* Drop every trace of a session that never became usable.
|
|
17066
|
+
*
|
|
17067
|
+
* The runner has already torn itself down (failStartup / handleExit); what
|
|
17068
|
+
* remains is server-side bookkeeping that would otherwise leave a dead
|
|
17069
|
+
* session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt`
|
|
17070
|
+
* marker that would suppress the mtime collision signal on the NEXT resume —
|
|
17071
|
+
* i.e. it would help hide the very owner we just collided with.
|
|
17072
|
+
*/
|
|
17073
|
+
abandonFailedStart(sessionId) {
|
|
17074
|
+
this.forgetSession(sessionId);
|
|
17075
|
+
}
|
|
17010
17076
|
enrichResumedSessionAsync(sessionId, projectPath, conv) {
|
|
17011
17077
|
try {
|
|
17012
17078
|
if (!this.sessionStore.getManaged(sessionId)) return;
|