@echomem/mcp 1.4.27 → 1.4.28

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.
@@ -77,6 +77,32 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
77
77
  function renderBridgeIssue() {
78
78
  renderReportIssue("BRIDGE_UNREACHABLE", "The local bridge stopped answering before your report was ready. Your terminal may have closed or the process may have stopped.");
79
79
  }
80
+ function renderDiscoveryCheck(slow) {
81
+ void slow;
82
+ setCityMode(false);
83
+ setExtractMode(false);
84
+ setReadyMode(true);
85
+ setHead("Loading", "Working");
86
+ app.className = "discoveryLoader";
87
+ app.innerHTML = '<div class="discoverySpinner" role="status" aria-label="Loading"><span aria-hidden="true"></span></div>';
88
+ }
89
+ function renderDiscoveryCheckIssue(error) {
90
+ setCityMode(false);
91
+ setExtractMode(false);
92
+ setReadyMode(true);
93
+ setHead("Session refresh paused", "Needs attention");
94
+ app.className = "loading-panel";
95
+ app.innerHTML =
96
+ '<div style="padding:24px"><h2>Echo could not refresh session status</h2>' +
97
+ '<p class="helper">Keep the terminal open and try the local check again.</p>' +
98
+ (error ? '<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(error) + '</code></details>' : '') +
99
+ '<div class="actions"><button id="retryDiscoveryCheck" class="primary">Try again</button></div></div>';
100
+ var retry = document.getElementById("retryDiscoveryCheck");
101
+ if (retry) retry.onclick = function () {
102
+ statsPollStarted = false;
103
+ void waitForStats();
104
+ };
105
+ }
80
106
  function renderLogoutPending() {
81
107
  setCityMode(false);
82
108
  setHead("Signing out locally", "Working");
@@ -96,30 +122,36 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
96
122
  }
97
123
  async function waitForStats() {
98
124
  if (statsPollStarted) return;
125
+ var statsEpoch = accountStateEpoch;
99
126
  statsPollStarted = true;
100
127
  setCityMode(false);
101
128
  setExtractMode(false);
102
129
  setReadyMode(true);
103
- setHead("Checking extraction", "Working");
104
- app.className = "loading-panel";
105
- app.innerHTML = '<div style="padding:24px"><h2>Checking for active work</h2><p class="helper">Echo is reconnecting to any extraction already running on this machine.</p></div>';
130
+ renderDiscoveryCheck(false);
106
131
  try {
107
132
  if (await resumeExistingExtraction()) return;
133
+ if (statsEpoch !== accountStateEpoch || !connected) return;
108
134
  var started = Date.now();
109
135
  var lastError = "";
110
136
  for (;;) {
111
- if (decisionMade) return;
137
+ if (decisionMade || statsEpoch !== accountStateEpoch || !connected) return;
112
138
  try {
113
139
  var res = await fetchWithTimeout("/stats?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 3000);
140
+ if (statsEpoch !== accountStateEpoch || !connected) return;
114
141
  if (res.status === 200) {
115
- stats = await res.json();
116
- if (decisionMade) return;
142
+ var nextStats = await res.json();
143
+ if (decisionMade || statsEpoch !== accountStateEpoch || !connected) return;
144
+ stats = nextStats;
117
145
  if (connected && !billingStatus && !billingStatusLoading) {
118
146
  try { await refreshBillingStatus(); } catch (_) {}
119
- if (decisionMade) return;
147
+ if (decisionMade || statsEpoch !== accountStateEpoch || !connected) return;
120
148
  }
121
149
  renderDashboard();
122
- if (stats && stats.partial) { await sleep(1200); continue; }
150
+ if (stats && stats.partial) {
151
+ await sleep(1200);
152
+ if (statsEpoch !== accountStateEpoch || !connected) return;
153
+ continue;
154
+ }
123
155
  return;
124
156
  }
125
157
  if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
@@ -172,7 +204,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
172
204
  var discovery = stats && stats.discovery ? stats.discovery : {};
173
205
  var isPartial = !!(stats && stats.partial);
174
206
  var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
175
- var eta = migratable && migratable.eta ? migratable.eta : {};
176
207
  var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
177
208
  var sessions = stats && stats.sessions ? stats.sessions : {};
178
209
  // The "quick" phase counts only this device's local ledger; it does not know what the account
@@ -183,25 +214,36 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
183
214
  var knownDone = pendingTrusted && pending === 0;
184
215
  var canExtract = pendingTrusted && pending > 0;
185
216
  var pendN = hasPendingCount ? pending : 0;
186
- var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || "a couple of minutes";
187
217
  var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
188
218
  var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
189
219
  if (connected && !billingStatusLoading && !setupPlanConfirmed()) {
190
220
  renderPlanGate();
191
221
  return;
192
222
  }
223
+ var importableNow = canExtract ? Math.min(pendN, historicalSelectionLimit()) : 0;
224
+ var planLimited = canExtract && importableNow < pendN;
225
+ var deferredByPlan = planLimited ? pendN - importableNow : 0;
226
+ var currentPlan = String(billingStatus && billingStatus.plan || "").toLowerCase();
227
+ var currentPlanLabel = paidRecallPlan(currentPlan)
228
+ ? currentPlan.charAt(0).toUpperCase() + currentPlan.slice(1) + " Echo"
229
+ : "Original Echo";
193
230
  setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
194
231
  var headlineHtml = !pendingTrusted
195
232
  ? "Counting your <strong>new conversations…</strong>"
196
233
  : (knownDone
197
234
  ? "You are <strong>all caught up.</strong>"
198
- : "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review.");
235
+ : (planLimited
236
+ ? "<strong>" + esc(number(pendN)) + " sessions</strong> found."
237
+ : "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review."));
199
238
  var sub = !pendingTrusted
200
239
  ? "Echo is matching your local history against what is already in memory."
201
- : (knownDone
240
+ : (knownDone
202
241
  ? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing, so Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
203
- : "Nothing uploads until you confirm.");
204
- var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start memory extraction" : "All caught up");
242
+ : (planLimited
243
+ ? (importableNow > 0
244
+ ? currentPlanLabel + " can import " + number(importableNow) + " of them. Choose which ones to bring into memory; the other " + number(deferredByPlan) + " stay on this Mac."
245
+ : currentPlanLabel + " has no coding-session imports remaining. These sessions stay on this Mac.")
246
+ : ""));
205
247
  // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
206
248
  // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
207
249
  // Later polls only patch the text/counts in place; the iframe stays mounted.
@@ -217,6 +259,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
217
259
  // not a display-size headline.
218
260
  app.innerHTML =
219
261
  setupJourney(4) +
262
+ '<aside class="readyUtilityNav" aria-label="Account and plan settings">' +
263
+ '<div class="planGateAccount readyAccountNav" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></div>' +
264
+ '<div class="readyPlanNav" aria-label="Current plan"><span class="readyPlanNavLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
265
+ '</aside>' +
220
266
  '<div id="exBanner"></div>' +
221
267
  '<section class="readyDecision extractionReady">' +
222
268
  '<div class="readyHero">' +
@@ -234,10 +280,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
234
280
  '</div>' +
235
281
  '<p class="ctaMeta" id="exEta"></p>' +
236
282
  '</div>' +
237
- '<aside class="readySettings" aria-label="Account and plan settings">' +
238
- '<div class="readySetting readyAccountSetting"><span class="readySettingLabel">Account</span><div class="setupAccountSlot" id="setupAccount"></div></div>' +
239
- '<div class="readySetting readyPlanSetting"><span class="readySettingLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
240
- '</aside>' +
241
283
  '</section>' +
242
284
  '<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
243
285
  document.getElementById("migrate").onclick = openSessionPicker;
@@ -266,7 +308,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
266
308
  // Reassurances live at the moment of commitment — right under the button.
267
309
  document.getElementById("exEta").innerHTML = pendingTrusted
268
310
  ? (canExtract
269
- ? '<span class="cm">&asymp; ' + esc(etaLabel) + '</span>'
311
+ ? ''
270
312
  : 'Nothing new to extract right now.')
271
313
  : 'Echo is checking local history against saved memory&hellip;';
272
314
  renderSetupPlan(canExtract);
@@ -277,9 +319,25 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
277
319
  var migrateBtn = document.getElementById("migrate");
278
320
  if (pendingTrusted) {
279
321
  migrateBtn.style.display = "";
280
- if (canExtract) ensureSessionSelection();
281
- migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
282
- migrateBtn.innerHTML = '<span>' + esc(canExtract ? "Review " + number(pendN) + " sessions" : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
322
+ var candidatesReady = !canExtract || candidateSessions().length > 0;
323
+ if (canExtract && candidatesReady) ensureSessionSelection();
324
+ // This button opens the picker; zero selected belongs to the picker's Start button.
325
+ migrateBtn.disabled = canExtract && (
326
+ !setupPlanConfirmed() ||
327
+ !candidatesReady ||
328
+ historicalSelectionLimit() === 0
329
+ );
330
+ migrateBtn.innerHTML = '<span>' + esc(
331
+ canExtract
332
+ ? (candidatesReady
333
+ ? (importableNow === 0
334
+ ? "No imports available"
335
+ : (planLimited
336
+ ? "Choose " + number(importableNow) + " of " + number(pendN) + " sessions"
337
+ : "Review " + number(pendN) + " sessions"))
338
+ : "Preparing session list…")
339
+ : "Finish setup"
340
+ ) + '</span>' + (canExtract && candidatesReady ? setupIcon("arrow-right") : "");
283
341
  migrateBtn.onclick = canExtract ? openSessionPicker : skip;
284
342
  } else {
285
343
  migrateBtn.style.display = "none";
@@ -311,9 +369,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
311
369
  decisionMade = true;
312
370
  extractionEnded = false;
313
371
  setCityMode(false);
372
+ setScanMode(false);
373
+ setReadyMode(false);
374
+ setExtractMode(true);
314
375
  setHead("Extracting", "Starting");
315
- app.className = "";
316
- app.innerHTML = '<div class="miniLoad"><h2>Starting extraction…</h2><div class="miniBar"><i></i></div></div>';
376
+ app.className = "extractionStartStage";
377
+ app.innerHTML =
378
+ setupJourney(4) +
379
+ '<div class="discoverySpinner" role="status" aria-label="Starting import"><span aria-hidden="true"></span></div>';
317
380
  var started;
318
381
  try {
319
382
  started = await postJson("/migrate", { conversationKeys: selectedKeys });
@@ -327,8 +390,34 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
327
390
  await pollProgress();
328
391
  }
329
392
  async function resumeExistingExtraction() {
393
+ var resumeEpoch = accountStateEpoch;
394
+ var existing = null;
395
+ var lastError = "";
396
+ for (var attempt = 0; attempt < 3; attempt++) {
397
+ try {
398
+ var progressResponse = await fetchWithTimeout(
399
+ "/progress?nonce=" + encodeURIComponent(nonce),
400
+ { credentials: "omit", cache: "no-store" },
401
+ 3000
402
+ );
403
+ if (!progressResponse.ok) throw new Error(await progressResponse.text() || ("HTTP " + progressResponse.status));
404
+ existing = await progressResponse.json();
405
+ break;
406
+ } catch (error) {
407
+ if (resumeEpoch !== accountStateEpoch || !connected) return false;
408
+ lastError = error && error.message ? error.message : String(error);
409
+ if (attempt < 2) {
410
+ renderDiscoveryCheck(true);
411
+ await sleep(500 * (attempt + 1));
412
+ }
413
+ }
414
+ }
415
+ if (!existing) {
416
+ renderDiscoveryCheckIssue(lastError);
417
+ return true;
418
+ }
330
419
  try {
331
- var existing = await getJson("/progress");
420
+ if (resumeEpoch !== accountStateEpoch || !connected) return false;
332
421
  if (!existing || existing.status === "idle") return false;
333
422
  decisionMade = true;
334
423
  extractionEnded = false;
@@ -338,21 +427,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
338
427
  void pollProgress();
339
428
  }
340
429
  return true;
341
- } catch (_) {
342
- // Stats cannot prove that no import is running. Stay in the recovery view until
343
- // the authoritative progress endpoint answers instead of exposing a second start.
344
- decisionMade = true;
345
- extractionEnded = false;
346
- extractMounted = false;
347
- renderProgress({
348
- status: "running",
349
- total: 0,
350
- completed: 0,
351
- failed: 0,
352
- extracted: 0,
353
- latest: "Reconnecting to the active extraction..."
354
- });
355
- void pollProgress();
430
+ } catch (error) {
431
+ renderDiscoveryCheckIssue(error && error.message ? error.message : String(error));
356
432
  return true;
357
433
  }
358
434
  }
@@ -434,11 +510,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
434
510
  ? '<p class="exSub">' + esc(progress.latest) + '</p>'
435
511
  : "";
436
512
  t.innerHTML =
437
- '<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
513
+ '<div class="resultStage ' + (done ? "firstRecallStage ok" : "warn") + '">' +
438
514
  '<div class="resultTop">' +
439
515
  '<div>' +
440
516
  (done
441
- ? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span></h2>' + completionNote
517
+ ? '<h2 class="savedHeading"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span></h2>' + completionNote
442
518
  : '<h2 class="siteHeadline">Some conversations need <strong>another pass.</strong></h2>' +
443
519
  '<p class="exSub">' + stopDetail + '</p>') +
444
520
  '</div>' +
@@ -714,7 +790,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
714
790
  var edit = document.getElementById("editSessions");
715
791
  if (edit) edit.onclick = openSessionPicker;
716
792
  var migrateBtn = document.getElementById("migrate");
717
- if (migrateBtn) migrateBtn.disabled = selected.length === 0 || !setupPlanConfirmed();
793
+ if (migrateBtn) migrateBtn.disabled =
794
+ candidates.length === 0 ||
795
+ !setupPlanConfirmed() ||
796
+ historicalSelectionLimit() === 0;
718
797
  }
719
798
  function updateSessionPickerSelectionUi(dialog, input, limit) {
720
799
  var selectedCount = selectedSessionKeyList().length;
@@ -740,13 +819,46 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
740
819
  '<span class="sessionPickerLimitEyebrow">Plan limit</span>' +
741
820
  '<h3>Plan limit reached.</h3>' +
742
821
  '<p>Upgrade your plan to select and import more conversations.</p>' +
743
- '<div class="sessionPickerLimitActions"><a id="upgradeSessionPlan" class="button primary" href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">' + linkLabel + '</a>' +
822
+ '<div class="sessionPickerLimitActions"><button type="button" id="upgradeSessionPlan" class="button primary">' + linkLabel + '</button>' +
744
823
  (canDismiss ? '<button type="button" class="textButton" id="dismissSessionLimit">Keep editing</button>' : "") + '</div>' +
745
824
  '</div>';
746
825
  }
826
+ async function openSessionPickerPlanAction() {
827
+ var currentPlan = String(billingStatus && billingStatus.plan || "").toLowerCase();
828
+ if (currentPlan === "power" || currentPlan === "team" || currentPlan === "enterprise") {
829
+ setSessionPickerStatus("Opening secure plan options…");
830
+ var portal = await openHostedPlanOptions();
831
+ setSessionPickerStatus(portal
832
+ ? "Plan options opened in Stripe. This page will update automatically."
833
+ : "Could not open plan options. Try again.");
834
+ return;
835
+ }
836
+ var targetPlan = currentPlan === "pro" ? "power" : "pro";
837
+ setSessionPickerStatus("Opening secure Stripe checkout…");
838
+ var checkout = await openHostedBilling(
839
+ "/billing-checkout",
840
+ { plan: targetPlan, trial: !billingStatus || billingStatus.trialAvailable !== false },
841
+ "Creating your secure checkout…",
842
+ "Checkout opened in Stripe. Keep this local page open; Echo will confirm here automatically."
843
+ );
844
+ if (!checkout) {
845
+ setSessionPickerStatus("Could not open Stripe checkout. Try again.");
846
+ return;
847
+ }
848
+ billingActivationPendingPlan = targetPlan;
849
+ if (typeof checkout.sessionId === "string") {
850
+ rememberCheckoutSessionId(checkout.sessionId);
851
+ billingSyncAttempts = 0;
852
+ billingSyncLastAttemptAt = 0;
853
+ }
854
+ document.title = "Finish checkout — Echo setup is waiting";
855
+ setSessionPickerStatus("Stripe checkout opened. Return here when you finish; this page updates automatically.");
856
+ }
747
857
  function wireSessionPickerUpgrade() {
748
- var upgrade = document.getElementById("upgradeSessionPlan");
749
- if (upgrade) upgrade.onclick = function () { startBillingPoll(); };
858
+ ["upgradeSessionPlan", "upgradeSessionPlanInline"].forEach(function (id) {
859
+ var upgrade = document.getElementById(id);
860
+ if (upgrade) upgrade.onclick = function () { void openSessionPickerPlanAction(); };
861
+ });
750
862
  var dismiss = document.getElementById("dismissSessionLimit");
751
863
  if (dismiss) dismiss.onclick = hideSessionPickerLimitNotice;
752
864
  }
@@ -780,6 +892,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
780
892
  var all = candidateSessions();
781
893
  var selected = selectedSessionKeyList();
782
894
  var limit = Math.min(all.length, historicalSelectionLimit());
895
+ var planLimited = all.length > limit;
896
+ var inlineUpgrade = planLimited && limit > 0
897
+ ? '<button type="button" id="upgradeSessionPlanInline" class="sessionPickerUpgrade">Upgrade</button>'
898
+ : "";
783
899
  var query = sessionPickerQuery.trim().toLowerCase();
784
900
  var filtered = all.filter(function (session) {
785
901
  if (sessionPickerSource !== "all" && session.source !== sessionPickerSource) return false;
@@ -805,7 +921,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
805
921
  : "";
806
922
  dialog.innerHTML =
807
923
  '<div class="sessionPickerShell">' +
808
- '<header class="sessionPickerHead"><div><h2 id="sessionPickerTitle">Choose conversations</h2><p>Up to ' + esc(number(limit)) + ' · newest selected first</p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
924
+ '<header class="sessionPickerHead"><div><h2 id="sessionPickerTitle">Choose conversations</h2><p class="sessionPickerSummary"><span class="sessionPickerQuota">Up to <strong>' + esc(number(limit)) + '</strong></span>' + inlineUpgrade + '<span class="sessionPickerOrder">&middot; newest selected first</span></p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
809
925
  '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
810
926
  '<label class="sessionSourceFilter"><span>Source</span><select id="sessionPickerSource"><option value="all"' + (sessionPickerSource === "all" ? " selected" : "") + '>All sources</option><option value="codex"' + (sessionPickerSource === "codex" ? " selected" : "") + '>Codex</option><option value="claude-code"' + (sessionPickerSource === "claude-code" ? " selected" : "") + '>Claude Code</option></select></label>' +
811
927
  '</div>' +
@@ -866,16 +982,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
866
982
  else dialog.setAttribute("open", "");
867
983
  }
868
984
  function accountInitials(account) {
869
- var label = String(account && (account.displayName || account.email) || "EchoMem").trim();
870
- return label.split(/\s+/).slice(0, 2).map(function (part) { return part.charAt(0).toUpperCase(); }).join("") || "E";
985
+ var email = String(account && account.email || "").trim();
986
+ var label = email ? email.split("@")[0] : "EchoMem";
987
+ return label.split(/[\s._-]+/).slice(0, 2).map(function (part) { return part.charAt(0).toUpperCase(); }).join("") || "E";
871
988
  }
872
989
  function renderAccountChoice() {
873
990
  var slot = document.getElementById("setupAccount");
874
991
  if (!slot) return;
875
992
  var account = billingStatus && billingStatus.account;
876
- var name = account && account.displayName ? account.displayName : "Connected EchoMem account";
877
- var email = account && account.email ? account.email : "This import will be saved to the account connected on this device.";
878
- var accountLabel = account && account.email ? account.email : name;
993
+ var accountLabel = account && account.email ? account.email : "Connected account";
879
994
  var avatarUrl = account && account.avatarUrl ? String(account.avatarUrl) : "";
880
995
  var avatar = '<span class="setupAccountAvatar" aria-hidden="true">' +
881
996
  '<span>' + esc(accountInitials(account)) + '</span>' +
@@ -884,7 +999,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
884
999
  slot.innerHTML =
885
1000
  '<section class="setupAccountCard" aria-label="Connected EchoMem account">' +
886
1001
  avatar +
887
- '<span class="setupAccountCopy"><small>Logged in as</small><strong>' + esc(accountLabel) + '</strong><span>' + esc(email) + '</span></span>' +
1002
+ '<span class="setupAccountCopy"><small>Logged in as</small><strong>' + esc(accountLabel) + '</strong></span>' +
888
1003
  '<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Switch</button>' +
889
1004
  '</section>';
890
1005
  var change = document.getElementById("changeSetupAccount");
@@ -892,8 +1007,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
892
1007
  }
893
1008
  function renderAccountSwitchConfirm() {
894
1009
  var account = billingStatus && billingStatus.account;
895
- var accountLabel = account && (account.email || account.displayName)
896
- ? (account.email || account.displayName)
1010
+ var accountLabel = account && account.email
1011
+ ? account.email
897
1012
  : "the connected account";
898
1013
  accountSwitchPending = true;
899
1014
  setCityMode(false);
@@ -921,19 +1036,29 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
921
1036
  async function switchSetupAccount() {
922
1037
  var button = document.getElementById("confirmAccountSwitch");
923
1038
  if (button) { button.disabled = true; button.textContent = "Signing out..."; }
1039
+ accountStateEpoch++;
1040
+ stopBillingPoll();
1041
+ progressPollRunId++;
924
1042
  try {
925
- await postJson("/logout", {}, 12000);
1043
+ var logoutResult = await postJson("/logout", {}, 12000);
1044
+ if (!logoutResult || logoutResult.connected !== false) {
1045
+ throw new Error("Echo did not confirm that this device signed out.");
1046
+ }
926
1047
  accountSwitchPending = false;
927
1048
  stats = null;
1049
+ statsSlow = false;
928
1050
  billingStatus = null;
1051
+ billingStatusLoading = false;
929
1052
  connected = false;
1053
+ rememberSetupPlanChoice("");
930
1054
  selectedSessionKeys = Object.create(null);
931
1055
  sessionSelectionTouched = false;
932
1056
  localAuthEmail = "";
933
1057
  localAuthTermsAccepted = false;
934
1058
  localAuthAgeConfirmed = false;
935
- renderLocalLogin("");
1059
+ renderLocalLogin("Signed out. Enter the email for the account you want to use.");
936
1060
  } catch (error) {
1061
+ if (connected) startBillingPoll();
937
1062
  if (button) { button.disabled = false; button.textContent = "Sign out & switch"; }
938
1063
  setLocalAuthStatus(error && error.message ? error.message : "Could not switch accounts.", "error");
939
1064
  }
@@ -1017,12 +1142,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1017
1142
  '<p>' + esc(lifecycle || "Your paid plan is active.") + '</p>' +
1018
1143
  '</section>';
1019
1144
  }
1020
- function setupPricingUrl(plan, startTrial) {
1021
- var paidPlan = plan === "power" ? "power" : "pro";
1022
- return (billingStatus && billingStatus.pricingUrl)
1023
- ? billingStatus.pricingUrl + (startTrial ? "&start=" + paidPlan + "&trial=" + (billingStatus.trialAvailable === false ? "0" : "1") : "")
1024
- : "https://echoknows.com/account?source=mcp_onboarding" + (startTrial ? "&start=" + paidPlan + "&trial=1" : "");
1025
- }
1026
1145
  function setupPlanConfirmed() {
1027
1146
  return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
1028
1147
  }
@@ -1038,7 +1157,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1038
1157
  sticker: "/hud-assets/echo-pricing-power-sticker.png",
1039
1158
  stickerAlt: "Power Echo arrives with a gold key and a crew of notebook helpers.",
1040
1159
  features: [
1041
- "2,000 coding sessions",
1160
+ "2,000 past chats",
1042
1161
  "agent-heavy memory",
1043
1162
  "2,000 memory recalls / week"
1044
1163
  ]
@@ -1054,7 +1173,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1054
1173
  stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
1055
1174
  popular: true,
1056
1175
  features: [
1057
- "500 coding sessions",
1176
+ "500 past chats",
1058
1177
  "daily memory updates",
1059
1178
  "500 memory recalls / week"
1060
1179
  ]
@@ -1272,23 +1391,37 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1272
1391
  }
1273
1392
  }
1274
1393
  async function refreshBillingStatus() {
1275
- if (billingStatusLoading) return;
1394
+ if (billingStatusLoading || !connected) return;
1395
+ var billingEpoch = accountStateEpoch;
1276
1396
  billingStatusLoading = true;
1277
1397
  try {
1278
1398
  await maybeSyncPendingCheckout();
1279
- billingStatus = await getJson("/billing-status");
1399
+ if (billingEpoch !== accountStateEpoch || !connected) return;
1400
+ var nextBillingStatus = await getJson("/billing-status");
1401
+ if (billingEpoch !== accountStateEpoch || !connected) return;
1402
+ billingStatus = nextBillingStatus;
1280
1403
  billingLastCheckedAt = Date.now();
1281
1404
  billingCheckMessage = "";
1282
- renderSetupPlan(true);
1283
- renderAccountChoice();
1284
- renderSessionSelection(true);
1405
+ if (readyStage === "sessions" && setupPlanConfirmed()) {
1406
+ // Keep the visible promise in sync with the authoritative plan/quota response.
1407
+ // renderDashboard patches the mounted shell without recreating the page. If the
1408
+ // picker is already open after an upgrade, refresh its quota/action in place too.
1409
+ renderDashboard();
1410
+ var openPicker = document.getElementById("sessionPicker");
1411
+ if (openPicker && openPicker.open) renderSessionPicker();
1412
+ } else {
1413
+ renderSetupPlan(true);
1414
+ renderAccountChoice();
1415
+ renderSessionSelection(true);
1416
+ }
1285
1417
  if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Original still works.");
1286
1418
  } catch (_) {
1419
+ if (billingEpoch !== accountStateEpoch || !connected) return;
1287
1420
  billingLastCheckedAt = Date.now();
1288
1421
  billingCheckMessage = "Could not check payment yet. Echo will keep trying automatically.";
1289
1422
  setSetupPlanStatus("Plan check unavailable. Original still works.");
1290
1423
  } finally {
1291
- billingStatusLoading = false;
1424
+ if (billingEpoch === accountStateEpoch) billingStatusLoading = false;
1292
1425
  }
1293
1426
  }
1294
1427
  function refreshBillingOnFocus() { void refreshBillingStatus(); }
@@ -1348,7 +1481,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1348
1481
  }
1349
1482
  }
1350
1483
  async function openHostedPlanOptions() {
1351
- await openHostedBilling(
1484
+ return await openHostedBilling(
1352
1485
  "/billing-portal",
1353
1486
  {},
1354
1487
  "Opening secure plan management…",
@@ -1359,9 +1492,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1359
1492
  var questions = recallQuestions();
1360
1493
  var primary = questions[0];
1361
1494
  return '<div class="tryCard">' +
1362
- '<h3 class="tryTitle">Let Echo prove it remembers</h3>' +
1495
+ '<h3 class="tryTitle">Take it into a fresh session.</h3>' +
1363
1496
  '<article class="recallPrompt recallPromptPrimary" data-recall-prompt="' + esc(primary) + '">' +
1364
- '<div class="recallPromptHead"><span>Your question</span><button type="button" class="recallCopy" data-recall-copy aria-label="Copy your first recall question">' + setupIcon("copy") + '<span>Copy</span></button></div>' +
1497
+ '<div class="recallPromptHead"><span>Try this</span><button type="button" class="recallCopy" data-recall-copy aria-label="Copy your first recall question">' + setupIcon("copy") + '<span>Copy</span></button></div>' +
1365
1498
  '<p class="recallPromptText">' + esc(primary) + '</p>' +
1366
1499
  '<div class="recallPromptActions" aria-label="Try your first memory recall">' +
1367
1500
  '<button type="button" class="recallAgent recallAgentPrimary" data-recall-codex><span class="recallAgentIcon"><img src="/hud-assets/codex.svg" alt="" /></span><span>Ask in Codex</span></button>' +