@echomem/mcp 1.4.18 → 1.4.19

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/hud/web.js CHANGED
@@ -678,6 +678,85 @@ export const HUD_HTML = String.raw `<!doctype html>
678
678
  box-shadow: 0 12px 20px -16px rgba(122, 36, 30, 0.95);
679
679
  }
680
680
  .billing-link:hover { filter: brightness(0.96); }
681
+ .quota-card {
682
+ display: none;
683
+ margin: 0 0 10px;
684
+ padding: 12px 13px 13px;
685
+ border: 1px solid rgba(26, 58, 143, 0.18);
686
+ border-radius: 12px;
687
+ background: rgba(255, 255, 255, 0.72);
688
+ box-shadow: 0 1px 5px rgba(26, 58, 143, 0.05);
689
+ }
690
+ .quota-head {
691
+ display: flex;
692
+ align-items: center;
693
+ gap: 7px;
694
+ margin-bottom: 9px;
695
+ }
696
+ .quota-title {
697
+ color: var(--ink);
698
+ font-size: 10.5px;
699
+ font-weight: 850;
700
+ letter-spacing: 0.015em;
701
+ }
702
+ .quota-plan {
703
+ border-radius: 999px;
704
+ background: rgba(26, 58, 143, 0.07);
705
+ padding: 2px 7px;
706
+ color: var(--blue);
707
+ font-size: 9px;
708
+ font-weight: 850;
709
+ text-transform: uppercase;
710
+ }
711
+ .quota-manage {
712
+ margin-left: auto;
713
+ border: 0;
714
+ padding: 0;
715
+ background: transparent;
716
+ color: var(--faint);
717
+ font-family: inherit;
718
+ font-size: 9.5px;
719
+ font-weight: 750;
720
+ cursor: pointer;
721
+ }
722
+ .quota-manage:hover { color: var(--blue); }
723
+ .quota-list { display: grid; gap: 9px; }
724
+ .quota-row { --quota-color: var(--blue); }
725
+ .quota-row.is-warn { --quota-color: #c98712; }
726
+ .quota-row.is-limit { --quota-color: #c7372f; }
727
+ .quota-line, .quota-subline {
728
+ display: flex;
729
+ align-items: baseline;
730
+ justify-content: space-between;
731
+ gap: 10px;
732
+ }
733
+ .quota-name { color: var(--muted); font-size: 10px; font-weight: 720; }
734
+ .quota-value {
735
+ color: var(--ink);
736
+ font-family: "JetBrains Mono", ui-monospace, monospace;
737
+ font-size: 9.5px;
738
+ font-weight: 760;
739
+ white-space: nowrap;
740
+ }
741
+ .quota-track {
742
+ height: 4px;
743
+ margin-top: 4px;
744
+ overflow: hidden;
745
+ border-radius: 999px;
746
+ background: rgba(26, 58, 143, 0.11);
747
+ }
748
+ .quota-fill {
749
+ width: 0;
750
+ height: 100%;
751
+ border-radius: inherit;
752
+ background: var(--quota-color);
753
+ transition: width .28s ease, background .2s ease;
754
+ }
755
+ .quota-subline { margin-top: 3px; }
756
+ .quota-reset, .quota-percent { color: var(--faint); font-size: 8.5px; font-weight: 650; }
757
+ .quota-row.is-limit .quota-value,
758
+ .quota-row.is-limit .quota-percent { color: #9d2b25; font-weight: 850; }
759
+ .quota-row.is-unavailable .quota-fill { background: rgba(26, 58, 143, 0.14); }
681
760
  button.renew-btn {
682
761
  display: none;
683
762
  width: 100%;
@@ -838,6 +917,30 @@ export const HUD_HTML = String.raw `<!doctype html>
838
917
  </div>
839
918
  <div class="details">
840
919
  <div class="timeline-scrollbar hidden" id="timelineScrollbar" aria-hidden="true"><span class="timeline-thumb" id="timelineThumb"></span></div>
920
+ <section class="quota-card" id="quotacard" aria-label="Plan usage">
921
+ <div class="quota-head">
922
+ <span class="quota-title">Plan usage</span>
923
+ <span class="quota-plan" id="quotaplan">Free</span>
924
+ <button class="quota-manage" id="quotamanage" type="button">Manage</button>
925
+ </div>
926
+ <div class="quota-list">
927
+ <div class="quota-row" data-quota="processing">
928
+ <div class="quota-line"><span class="quota-name">Processing this week</span><span class="quota-value"></span></div>
929
+ <div class="quota-track"><div class="quota-fill"></div></div>
930
+ <div class="quota-subline"><span class="quota-reset"></span><span class="quota-percent"></span></div>
931
+ </div>
932
+ <div class="quota-row" data-quota="searches">
933
+ <div class="quota-line"><span class="quota-name">Memory searches</span><span class="quota-value"></span></div>
934
+ <div class="quota-track"><div class="quota-fill"></div></div>
935
+ <div class="quota-subline"><span class="quota-reset"></span><span class="quota-percent"></span></div>
936
+ </div>
937
+ <div class="quota-row" data-quota="historical">
938
+ <div class="quota-line"><span class="quota-name">Bulk history imports</span><span class="quota-value"></span></div>
939
+ <div class="quota-track"><div class="quota-fill"></div></div>
940
+ <div class="quota-subline"><span class="quota-reset">Lifetime limit</span><span class="quota-percent"></span></div>
941
+ </div>
942
+ </div>
943
+ </section>
841
944
  <div class="now-card" id="nowcard">
842
945
  <div class="now-sess">
843
946
  <img class="now-icon" id="nowicon" src="/assets/hud/codex.svg" alt="" />
@@ -903,6 +1006,9 @@ export const HUD_HTML = String.raw `<!doctype html>
903
1006
  const billingTitle = document.getElementById("billingtitle");
904
1007
  const billingCopy = document.getElementById("billingcopy");
905
1008
  const billingLink = document.getElementById("billinglink");
1009
+ const quotaCard = document.getElementById("quotacard");
1010
+ const quotaPlan = document.getElementById("quotaplan");
1011
+ const quotaManage = document.getElementById("quotamanage");
906
1012
  const renewBtn = document.getElementById("renewbtn");
907
1013
  const renewResult = document.getElementById("renewresult");
908
1014
  const renewMsg = document.getElementById("renewmsg");
@@ -1134,7 +1240,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1134
1240
  if (!id) return;
1135
1241
  fetch("/jump?id=" + encodeURIComponent(id)).catch(() => {});
1136
1242
  }
1137
- if (billingLink) billingLink.addEventListener("click", (e) => {
1243
+ function openPricing(e) {
1138
1244
  e.stopPropagation();
1139
1245
  const url = billingStatus && billingStatus.pricingUrl;
1140
1246
  if (!url) return;
@@ -1146,7 +1252,9 @@ export const HUD_HTML = String.raw `<!doctype html>
1146
1252
  .catch(() => {
1147
1253
  try { window.open(url, "_blank", "noopener"); } catch {}
1148
1254
  });
1149
- });
1255
+ }
1256
+ if (billingLink) billingLink.addEventListener("click", openPricing);
1257
+ if (quotaManage) quotaManage.addEventListener("click", openPricing);
1150
1258
  function toggleSessionDetails(id) {
1151
1259
  if (!id) return;
1152
1260
  const nextId = lastState && lastState.expandedId === id ? "auto" : id;
@@ -1614,6 +1722,18 @@ export const HUD_HTML = String.raw `<!doctype html>
1614
1722
  const previewExpanded = previewParams.get("expanded") === "1" || previewParams.get("pinned") === "1";
1615
1723
  if (previewParams.get("capture") === "1") hud.classList.add("capture-preview");
1616
1724
  if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
1725
+ const previewProcessingUsed = previewColor === "red" ? 200000 : previewColor === "amber" ? 163000 : 84000;
1726
+ const previewSearchUsed = previewColor === "red" ? 100 : previewColor === "amber" ? 82 : 32;
1727
+ billingStatus = {
1728
+ ok: true,
1729
+ state: "ok",
1730
+ plan: "pro",
1731
+ paid: true,
1732
+ pricingUrl: "https://yeahecho.com/pricing?source=hud_preview",
1733
+ memoryProcessingQuota: { used: previewProcessingUsed, limit: 200000, remaining: 200000 - previewProcessingUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
1734
+ memorySearchQuota: { used: previewSearchUsed, limit: 100, remaining: 100 - previewSearchUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
1735
+ historicalConversationQuota: { used: 191, limit: 250, remaining: 59 },
1736
+ };
1617
1737
  const sample = {
1618
1738
  client: "codex",
1619
1739
  label: "EchoMem-Chrome",
@@ -1824,25 +1944,96 @@ export const HUD_HTML = String.raw `<!doctype html>
1824
1944
 
1825
1945
  function renderBillingStatus() {
1826
1946
  if (!billingBanner) return;
1947
+ renderQuotaUsage();
1827
1948
  const state = billingStatus && billingStatus.state;
1828
- const blocked = state === "plan_required" || state === "quota_exceeded" || state === "billing_attention";
1949
+ const blocked = state === "quota_exceeded" || state === "billing_attention";
1829
1950
  billingBanner.style.display = blocked ? "block" : "none";
1830
1951
  billingBanner.classList.toggle("limit", state === "quota_exceeded");
1831
1952
  if (!blocked) return;
1832
- const title = state === "quota_exceeded"
1833
- ? "Recall limit reached"
1834
- : state === "plan_required"
1835
- ? "Recall paused · Free plan"
1836
- : billingStatus.message || "Billing needs attention";
1837
- const detail = state === "quota_exceeded"
1838
- ? "You used all Pro searches this month. Upgrade or wait for reset."
1839
- : state === "plan_required"
1840
- ? "Search memory requires Pro or Power. Saving still works."
1841
- : billingStatus.detail || "Open pricing to restore memory search.";
1953
+ const title = billingStatus.message || (state === "quota_exceeded" ? "Plan limit reached" : "Billing needs attention");
1954
+ const detail = billingStatus.detail || (state === "quota_exceeded"
1955
+ ? "A weekly limit has been reached. Wait for reset or change your plan."
1956
+ : "Open pricing to review your plan.");
1842
1957
  if (billingTitle) billingTitle.textContent = title;
1843
1958
  if (billingCopy) billingCopy.textContent = detail;
1844
1959
  }
1845
1960
 
1961
+ function renderQuotaUsage() {
1962
+ if (!quotaCard) return;
1963
+ const status = billingStatus || {};
1964
+ const quotaRows = [
1965
+ ["processing", status.memoryProcessingQuota, false],
1966
+ ["searches", status.memorySearchQuota, false],
1967
+ ["historical", status.historicalConversationQuota, true],
1968
+ ];
1969
+ const hasQuota = quotaRows.some((entry) => entry[1] && typeof entry[1] === "object");
1970
+ const visible = hasQuota && status.state !== "not_connected" && status.plan !== "unknown";
1971
+ quotaCard.style.display = visible ? "block" : "none";
1972
+ if (!visible) return;
1973
+ if (quotaPlan) quotaPlan.textContent = planLabel(status.plan || "free");
1974
+ for (const entry of quotaRows) renderQuotaRow(entry[0], entry[1], entry[2]);
1975
+ }
1976
+
1977
+ function renderQuotaRow(key, snapshot, lifetime) {
1978
+ if (!quotaCard) return;
1979
+ const row = quotaCard.querySelector('[data-quota="' + key + '"]');
1980
+ if (!row) return;
1981
+ const valueEl = row.querySelector(".quota-value");
1982
+ const fillEl = row.querySelector(".quota-fill");
1983
+ const resetEl = row.querySelector(".quota-reset");
1984
+ const percentEl = row.querySelector(".quota-percent");
1985
+ const used = quotaNumber(snapshot && snapshot.used);
1986
+ const limit = quotaNumber(snapshot && snapshot.limit);
1987
+ const available = used !== null && limit !== null;
1988
+ const unlimited = available && limit < 0;
1989
+ const percent = available && limit > 0 ? clamp(Math.round(used / limit * 100), 0, 100) : 0;
1990
+
1991
+ row.classList.toggle("is-unavailable", !available);
1992
+ row.classList.toggle("is-warn", available && !unlimited && percent >= 80 && percent < 100);
1993
+ row.classList.toggle("is-limit", available && !unlimited && percent >= 100);
1994
+ if (valueEl) {
1995
+ valueEl.textContent = unlimited
1996
+ ? formatQuotaNumber(used) + " / Unlimited"
1997
+ : (used === null ? "—" : formatQuotaNumber(used)) + " / " + (limit === null ? "—" : formatQuotaNumber(limit));
1998
+ }
1999
+ if (fillEl) fillEl.style.width = unlimited ? "0%" : percent + "%";
2000
+ if (percentEl) percentEl.textContent = !available
2001
+ ? "Usage unavailable"
2002
+ : unlimited
2003
+ ? "Unlimited"
2004
+ : percent >= 100
2005
+ ? "Limit reached"
2006
+ : percent + "%";
2007
+ if (resetEl) resetEl.textContent = lifetime ? "Lifetime limit" : quotaResetLabel(snapshot && snapshot.resetAt);
2008
+ row.setAttribute("aria-label", key + " usage " + (percentEl ? percentEl.textContent : ""));
2009
+ }
2010
+
2011
+ function quotaNumber(value) {
2012
+ return typeof value === "number" && isFinite(value) ? value : null;
2013
+ }
2014
+
2015
+ function formatQuotaNumber(value) {
2016
+ const n = Math.max(0, Number(value) || 0);
2017
+ if (n >= 1000000) return (n / 1000000).toFixed(n < 10000000 ? 1 : 0).replace(/\.0$/, "") + "M";
2018
+ if (n >= 1000) return (n / 1000).toFixed(n < 100000 ? 1 : 0).replace(/\.0$/, "") + "K";
2019
+ return Math.round(n).toLocaleString("en-US");
2020
+ }
2021
+
2022
+ function quotaResetLabel(value) {
2023
+ if (!value) return "Weekly reset";
2024
+ const resetAt = new Date(value);
2025
+ if (!isFinite(resetAt.getTime())) return "Weekly reset";
2026
+ if (resetAt.getTime() <= Date.now()) return "Resetting now";
2027
+ return "Resets " + resetAt.toLocaleString(undefined, {
2028
+ weekday: "short",
2029
+ month: "short",
2030
+ day: "numeric",
2031
+ hour: "numeric",
2032
+ minute: "2-digit",
2033
+ timeZoneName: "short",
2034
+ });
2035
+ }
2036
+
1846
2037
  function renderWhy(score, oldPct, repeatCount, sat, signal, color) {
1847
2038
  const rows = [];
1848
2039
  for (const row of junkBreakdown(score)) rows.push(row);
package/dist/index.js CHANGED
@@ -126,7 +126,7 @@ function formatUpgradeRequiredResult(error) {
126
126
  pricingUrl,
127
127
  });
128
128
  return [
129
- "ACTION REQUIRED: tell the user to start a 7-day Pro trial or choose a subscription at this exact URL:",
129
+ "ACTION REQUIRED: tell the user to start a 14-day Pro trial or choose a subscription at this exact URL:",
130
130
  pricingUrl,
131
131
  "When replying, include the exact URL above. Do not reply only with \"connect\" or \"upgrade\".",
132
132
  "",
@@ -545,10 +545,16 @@ class EchoMemApiClient {
545
545
  async fetchMemoryMap() {
546
546
  try {
547
547
  const enc = await this.encState(); // unencrypted → {enabled:false}; encrypted+locked → throws
548
- const today = new Date().toISOString().slice(0, 10);
549
- const response = await this.axios.post("/api/extension/memories/time-range", { startDate: "2000-01-01", endDate: today, limit: 200 }, { timeout: 6000 });
548
+ // Tool-list decoration is not a user-initiated search and must not spend
549
+ // one of the account's weekly searches. Build the best-effort map from
550
+ // the ordinary paginated memory listing instead.
551
+ const response = await this.axios.get("/api/extension/memories?limit=200", { timeout: 6000 });
550
552
  const data = enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
551
- const memories = Array.isArray(data?.memories) ? data.memories : [];
553
+ const memories = Array.isArray(data?.data)
554
+ ? data.data
555
+ : Array.isArray(data?.memories)
556
+ ? data.memories
557
+ : [];
552
558
  const keys = [...new Set(memories.map((m) => String(m?.keys || "").trim()).filter(Boolean))];
553
559
  if (!keys.length)
554
560
  return undefined;
@@ -789,6 +795,7 @@ class EchoMemApiClient {
789
795
  startDate,
790
796
  endDate,
791
797
  limit,
798
+ requestId: randomUUID(),
792
799
  });
793
800
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
794
801
  }
@@ -833,21 +840,41 @@ class EchoMemApiClient {
833
840
  // For an encrypted account, hand the server the key transiently in the X-Encryption-Key header
834
841
  // so it encrypts at rest (mirrors the extension's write path, spec §3.1a). Locked → LockedError.
835
842
  const enc = await this.encState();
836
- const config = enc.enabled && enc.key ? { headers: { "X-Encryption-Key": enc.key } } : undefined;
837
- const response = await this.axios.post("/api/extension/memories/ingest", {
838
- rawData,
839
- sourceUrl: parsed.url,
840
- source: parsed.source || "mcp_server",
841
- title: parsed.title,
842
- // Stable per-session id so multiple saves in this coding session group under one context.
843
- conversationKey: this.sessionId,
844
- passthrough: parsed.passthrough || false,
845
- triggerMessage: parsed.triggerMessage ||
846
- lastUserMessageFromMessages(parsed.messages) ||
847
- lastUserMessageFromConversationText(parsed.conversation),
848
- triggerMessageRole: parsed.triggerMessageRole || "user",
849
- }, config);
850
- return response.data;
843
+ const config = {
844
+ headers: {
845
+ "X-EchoMem-Request-Id": randomUUID(),
846
+ ...(enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : {}),
847
+ },
848
+ };
849
+ try {
850
+ const response = await this.axios.post("/api/extension/memories/ingest", {
851
+ rawData,
852
+ sourceUrl: parsed.url,
853
+ source: parsed.source || "mcp_server",
854
+ title: parsed.title,
855
+ // Stable per-session id so multiple saves in this coding session group under one context.
856
+ conversationKey: this.sessionId,
857
+ passthrough: parsed.passthrough || false,
858
+ triggerMessage: parsed.triggerMessage ||
859
+ lastUserMessageFromMessages(parsed.messages) ||
860
+ lastUserMessageFromConversationText(parsed.conversation),
861
+ triggerMessageRole: parsed.triggerMessageRole || "user",
862
+ }, config);
863
+ return response.data;
864
+ }
865
+ catch (error) {
866
+ if (axios.isAxiosError(error) && error.response?.status === 429) {
867
+ const data = error.response.data;
868
+ if (data?.error === "MEMORY_PROCESSING_QUOTA_EXCEEDED") {
869
+ const message = typeof data.message === "string"
870
+ ? data.message
871
+ : "Weekly memory-processing limit reached. This conversation was not saved.";
872
+ const upgrade = typeof data.upgradeUrl === "string" ? ` Upgrade: ${data.upgradeUrl}` : "";
873
+ throw new McpError(ErrorCode.InvalidRequest, `${message}${upgrade}`);
874
+ }
875
+ }
876
+ throw error;
877
+ }
851
878
  }
852
879
  async deleteMemory(args) {
853
880
  const parsed = deleteMemorySchema.parse(args ?? {});
@@ -900,6 +927,7 @@ class EchoMemApiClient {
900
927
  startDate: parsed.startDate,
901
928
  endDate: parsed.endDate,
902
929
  limit: parsed.limit,
930
+ requestId: randomUUID(),
903
931
  });
904
932
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
905
933
  }
@@ -918,6 +946,7 @@ class EchoMemApiClient {
918
946
  const response = await this.axios.post("/api/extension/memories/keywords", {
919
947
  keywords: parsed.keywords,
920
948
  limit: parsed.limit,
949
+ requestId: randomUUID(),
921
950
  });
922
951
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
923
952
  }
@@ -1362,7 +1391,7 @@ Details: ${m.details || "N/A"}`)
1362
1391
  rec.conversation_chars = text.length;
1363
1392
  rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
1364
1393
  }
1365
- const { success, memoriesExtracted, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
1394
+ const { success, memoriesExtracted, memoriesDiscarded, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
1366
1395
  if (!success)
1367
1396
  throw new Error(`EchoMem API Error: ${error}`);
1368
1397
  if (rec)
@@ -1374,7 +1403,7 @@ Details: ${m.details || "N/A"}`)
1374
1403
  typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
1375
1404
  typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
1376
1405
  "",
1377
- `To reload this capsule in a fresh session with Pro/Power recall: get_memories_by_context({ contextId: "${contextId}" })`,
1406
+ `To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
1378
1407
  ].filter(Boolean).join("\n");
1379
1408
  return { content: [{ type: "text", text }] };
1380
1409
  }
@@ -1396,9 +1425,12 @@ Details: ${m.details || "N/A"}`)
1396
1425
  .join("\n\n");
1397
1426
  const text = [
1398
1427
  `Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
1428
+ typeof memoriesDiscarded === "number" && memoriesDiscarded > 0
1429
+ ? `${memoriesDiscarded} additional memories were not stored because your active-memory limit was reached.`
1430
+ : "",
1399
1431
  list,
1400
1432
  saved.length
1401
- ? `Verify these captured the key facts. With Pro/Power recall, re-fetch this exact batch later by searching the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
1433
+ ? `Verify these captured the key facts. Re-fetch this exact batch later by searching the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
1402
1434
  : "",
1403
1435
  ].filter(Boolean).join("\n\n");
1404
1436
  return { content: [{ type: "text", text }] };
package/dist/migrate.js CHANGED
@@ -499,6 +499,11 @@ function parsePositiveIntFlag(value, name) {
499
499
  throw new Error(`${name} must be a positive integer.`);
500
500
  return n;
501
501
  }
502
+ export async function cancelQueuedImportSession(client, sessionId) {
503
+ if (!sessionId)
504
+ throw new Error("IMPORT_SESSION_ID_REQUIRED");
505
+ await client.patch(`/api/extension/import-sessions/${encodeURIComponent(sessionId)}`, { action: "cancel" });
506
+ }
502
507
  function isRecord(value) {
503
508
  return typeof value === "object" && value !== null && !Array.isArray(value);
504
509
  }
@@ -989,7 +994,18 @@ export async function startMigration(opts) {
989
994
  metricsFile,
990
995
  selection: opts.selection,
991
996
  });
992
- return { sessionId: session.id, runId, jobCount: session.jobs.length, metricsFile, ...(capped ? { capped } : {}), done };
997
+ const cancelQueued = async () => {
998
+ await cancelQueuedImportSession(client, session.id);
999
+ };
1000
+ return {
1001
+ sessionId: session.id,
1002
+ runId,
1003
+ jobCount: session.jobs.length,
1004
+ metricsFile,
1005
+ ...(capped ? { capped } : {}),
1006
+ done,
1007
+ cancelQueued,
1008
+ };
993
1009
  }
994
1010
  async function runJobs(args) {
995
1011
  const ledger = loadLedger();
@@ -1096,7 +1112,8 @@ async function runJobs(args) {
1096
1112
  }
1097
1113
  };
1098
1114
  try {
1099
- await Promise.all(Array.from({ length: Math.min(MIGRATE_CONCURRENCY, Math.max(1, total)) }, () => worker()));
1115
+ const workerCount = Math.min(MIGRATE_CONCURRENCY, Math.max(1, args.session.concurrency), Math.max(1, total));
1116
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
1100
1117
  }
1101
1118
  catch {
1102
1119
  failed++;
@@ -1236,7 +1253,7 @@ export async function cmdMigrate(flags) {
1236
1253
  },
1237
1254
  });
1238
1255
  if (h.capped)
1239
- console.log(c.yellow(`Your plan imports up to ${h.capped} at a time — importing the newest ${h.capped}; re-run migrate for older sessions.`));
1256
+ console.log(c.yellow(`Your plan has ${h.capped} historical slots remaining — importing the newest ${h.capped}. Upgrade to import more.`));
1240
1257
  console.log(c.dim(`Import session ${h.sessionId} — ${h.jobCount} jobs queued (the web dashboard can watch this live).`));
1241
1258
  console.log(c.dim(`Metrics: ${h.metricsFile}`));
1242
1259
  const r = await h.done;
@@ -1283,7 +1300,14 @@ async function createImportSession(client, sessions, bareId, userTz, signal) {
1283
1300
  }));
1284
1301
  const res = await client.post("/api/extension/import-sessions", { items }, signal ? { signal } : undefined);
1285
1302
  const data = res.data || {};
1286
- return { id: String(data.session?.id || ""), jobs: Array.isArray(data.jobs) ? data.jobs : [] };
1303
+ const serverConcurrency = Number(data.session?.concurrency);
1304
+ return {
1305
+ id: String(data.session?.id || ""),
1306
+ concurrency: Number.isFinite(serverConcurrency) && serverConcurrency > 0
1307
+ ? Math.min(MIGRATE_CONCURRENCY, Math.floor(serverConcurrency))
1308
+ : MIGRATE_CONCURRENCY,
1309
+ jobs: Array.isArray(data.jobs) ? data.jobs : [],
1310
+ };
1287
1311
  }
1288
1312
  /** Run one queued job: stream its transcript to the server, which extracts it. Retries transient locks/rate limits. */
1289
1313
  async function runImportJob(client, jobId, s, userTz, encKey) {
@@ -13,16 +13,27 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
13
13
  var statsSlow = false;
14
14
  var decisionMade = false;
15
15
  var connected = false;
16
+ var localHistoryConsentGranted = false;
16
17
  var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
18
+ var lastExtractionProgress = null; // restores the live extraction view if ending fails
19
+ var extractionRecoveryMessage = ""; // persists pause failures across resumed progress updates
17
20
  var exStartAt = 0; // extraction start (ms) — drives the countdown
18
21
  var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
19
22
  var extractionEnded = false; // user chose to leave the extraction flow before natural completion
23
+ var progressPollRunId = 0; // invalidates older progress loops when extraction resumes
20
24
  var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
21
25
  var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
22
26
  var authUrl = "";
27
+ var switchAccountUrl = "";
23
28
  var workspacePath = "";
24
29
  var billingStatus = null;
25
- var recallGateSkipped = false;
30
+ var billingStatusLoading = false;
31
+ var setupPlanChoice = "";
32
+ var selectedSessionKeys = Object.create(null);
33
+ var sessionSelectionTouched = false;
34
+ var sessionPickerQuery = "";
35
+ var sessionPickerSource = "all";
36
+ var billingPollTimer = null;
26
37
  var authWindow = null;
27
38
  var connectionPollStarted = false;
28
39
  var statsPollStarted = false;
@@ -34,6 +45,20 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
34
45
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
35
46
  });
36
47
  }
48
+ function setupIcon(name) {
49
+ var paths = {
50
+ "shield-check": '<path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/><path d="m8.8 12.1 2 2 4.4-4.5"/>',
51
+ "folder-read": '<path d="M3 7.5h6l2-2h4.5A2.5 2.5 0 0 1 18 8v1"/><path d="M3 7.5V18a2 2 0 0 0 2 2h7"/><circle cx="17" cy="15" r="3.5"/><path d="m19.5 17.5 2 2"/>',
52
+ "report": '<path d="M5 20V10"/><path d="M12 20V4"/><path d="M19 20v-7"/><path d="M3 20h18"/>',
53
+ "cloud-off": '<path d="m3 3 18 18"/><path d="M6.7 6.7A5.5 5.5 0 0 0 6.5 17H17"/><path d="M10.7 5.2A7 7 0 0 1 19 12.1 4 4 0 0 1 19.5 19"/>',
54
+ "list-check": '<rect x="4" y="4" width="16" height="16" rx="3"/><path d="m7.5 9 1.4 1.4L11.5 8"/><path d="M13.5 9h3"/><path d="m7.5 14 1.4 1.4 2.6-2.4"/><path d="M13.5 14h3"/>',
55
+ "pause": '<path d="M9 7v10"/><path d="M15 7v10"/>',
56
+ "check": '<path d="m6.5 12.2 3.5 3.5 7.5-7.5"/>',
57
+ "copy": '<rect x="8" y="8" width="11" height="11" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>',
58
+ "arrow-right": '<path d="M5 12h14"/><path d="m14 7 5 5-5 5"/>'
59
+ };
60
+ return '<svg class="setupGlyph" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (paths[name] || "") + '</g></svg>';
61
+ }
37
62
  function setHead(nextTitle, nextStatus) {
38
63
  title.textContent = nextTitle;
39
64
  status.textContent = nextStatus;
@@ -151,6 +176,11 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
151
176
  });
152
177
  }
153
178
  function onConnectClick() {
179
+ if (!localHistoryConsentGranted) {
180
+ renderLocalScanConsent();
181
+ setConsentStatus("Local history access is required before you can connect EchoMem or continue setup.", "required");
182
+ return;
183
+ }
154
184
  if (connected) { void waitForStats(); return; }
155
185
  if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
156
186
  startConnectionPoll();