@threadbase-sh/streamer 1.61.1 → 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/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
- interface FeatureFlagDefinition {
109
- /** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
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
- declare const FEATURE_FLAGS: readonly [{
134
- readonly id: "codexSystemPrompt";
135
- readonly description: string;
136
- readonly default: false;
137
- readonly env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT";
138
- }, {
139
- readonly id: "sessionRehydration";
140
- readonly description: string;
141
- readonly default: true;
142
- readonly env: "THREADBASE_FEATURE_SESSION_REHYDRATION";
143
- }, {
144
- readonly id: "liveActivityPush";
145
- readonly description: string;
146
- readonly default: false;
147
- readonly env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH";
148
- }, {
149
- readonly id: "e2ee";
150
- readonly description: string;
151
- readonly default: false;
152
- readonly env: "THREADBASE_FEATURE_E2EE";
153
- }, {
154
- readonly id: "ptyHost";
155
- readonly description: string;
156
- readonly default: false;
157
- readonly env: "THREADBASE_FEATURE_PTY_HOST";
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 = (typeof FEATURE_FLAGS)[number]["id"];
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 that never became usable, and hand back what
2513
- * it failed with.
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
- interface FeatureFlagDefinition {
109
- /** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
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
- declare const FEATURE_FLAGS: readonly [{
134
- readonly id: "codexSystemPrompt";
135
- readonly description: string;
136
- readonly default: false;
137
- readonly env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT";
138
- }, {
139
- readonly id: "sessionRehydration";
140
- readonly description: string;
141
- readonly default: true;
142
- readonly env: "THREADBASE_FEATURE_SESSION_REHYDRATION";
143
- }, {
144
- readonly id: "liveActivityPush";
145
- readonly description: string;
146
- readonly default: false;
147
- readonly env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH";
148
- }, {
149
- readonly id: "e2ee";
150
- readonly description: string;
151
- readonly default: false;
152
- readonly env: "THREADBASE_FEATURE_E2EE";
153
- }, {
154
- readonly id: "ptyHost";
155
- readonly description: string;
156
- readonly default: false;
157
- readonly env: "THREADBASE_FEATURE_PTY_HOST";
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 = (typeof FEATURE_FLAGS)[number]["id"];
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 that never became usable, and hand back what
2513
- * it failed with.
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
- return FEATURE_FLAGS.find((f) => f.id === id);
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 FEATURE_FLAGS) {
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 FEATURE_FLAGS.filter((f) => values[f.id] !== f.default).map((f) => f.id);
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 FEATURE_FLAGS.map(
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),
@@ -16753,7 +16801,7 @@ var StreamerServer = class {
16753
16801
  */
16754
16802
  getFeatureFlagsConfig() {
16755
16803
  return {
16756
- registry: FEATURE_FLAGS,
16804
+ registry: FEATURE_FLAG_LIST,
16757
16805
  values: this.featureFlags,
16758
16806
  sources: this.featureFlagSources
16759
16807
  };
@@ -16987,29 +17035,44 @@ var StreamerServer = class {
16987
17035
  });
16988
17036
  }
16989
17037
  /**
16990
- * Drop every trace of a session that never became usable, and hand back what
16991
- * it failed with.
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.
16992
17040
  *
16993
- * The runner has already torn itself down (failStartup / handleExit); what
16994
- * remains is server-side bookkeeping that would otherwise leave a dead
16995
- * session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt`
16996
- * marker that would suppress the mtime collision signal on the NEXT resume —
16997
- * i.e. it would help hide the very owner we just collided with.
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.
16998
17049
  */
16999
- abandonFailedStart(sessionId) {
17050
+ forgetSession(sessionId) {
17000
17051
  this.sessionStore.removeManaged(sessionId);
17001
17052
  this.selfPtyEndedAt.delete(sessionId);
17002
17053
  this.contendedSessions.delete(sessionId);
17003
17054
  try {
17004
17055
  this.managedSessionsRepo?.delete(sessionId);
17005
17056
  } catch (err) {
17006
- this.log.warn("[registry] failed to drop a failed start", {
17057
+ this.log.warn("[registry] failed to drop a session", {
17007
17058
  event: "registry.forget_failed",
17008
17059
  sessionId,
17009
17060
  err
17010
17061
  });
17011
17062
  }
17012
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
+ }
17013
17076
  enrichResumedSessionAsync(sessionId, projectPath, conv) {
17014
17077
  try {
17015
17078
  if (!this.sessionStore.getManaged(sessionId)) return;