terminalhire 0.37.0 → 0.38.0

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.
@@ -12012,6 +12012,7 @@ __export(spinner_seen_exports, {
12012
12012
  SEEN_MAX_WIDTHS: () => SEEN_MAX_WIDTHS,
12013
12013
  SEEN_TTL_MS: () => SEEN_TTL_MS,
12014
12014
  SEEN_WINDOW_SURFACES: () => SEEN_WINDOW_SURFACES,
12015
+ isAtCapacity: () => isAtCapacity,
12015
12016
  isSuppressed: () => isSuppressed,
12016
12017
  loadSeenHistory: () => loadSeenHistory,
12017
12018
  recordSurface: () => recordSurface,
@@ -12020,6 +12021,9 @@ __export(spinner_seen_exports, {
12020
12021
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, renameSync as renameSync7 } from "fs";
12021
12022
  import { join as join13, dirname as dirname4 } from "path";
12022
12023
  import { homedir as homedir11 } from "os";
12024
+ function isAtCapacity(history) {
12025
+ return Object.keys(history?.entries ?? {}).length >= SEEN_MAX_ENTRIES;
12026
+ }
12023
12027
  function seenFilePath() {
12024
12028
  const dir = process.env["TERMINALHIRE_DIR"] || join13(homedir11(), ".terminalhire");
12025
12029
  return join13(dir, "seen-history.json");
@@ -12113,7 +12117,7 @@ var init_spinner_seen = __esm({
12113
12117
  init_state_dir();
12114
12118
  SEEN_WINDOW_SURFACES = 10;
12115
12119
  SEEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
12116
- SEEN_MAX_ENTRIES = 500;
12120
+ SEEN_MAX_ENTRIES = 1500;
12117
12121
  SEEN_MAX_WIDTHS = 200;
12118
12122
  }
12119
12123
  });
@@ -13108,6 +13112,17 @@ function rotate(list, now, windowMs = ROTATE_WINDOW_MS) {
13108
13112
  const idx = Math.floor(now / windowMs) % list.length;
13109
13113
  return [...list.slice(idx), ...list.slice(0, idx)];
13110
13114
  }
13115
+ function lastSurfaceFor(result, lastSurfaceOf) {
13116
+ const id = result?.job?.id;
13117
+ if (typeof id !== "string" || id.length === 0) return UNSEEN;
13118
+ const v = lastSurfaceOf(id);
13119
+ return typeof v === "number" && Number.isFinite(v) ? v : UNSEEN;
13120
+ }
13121
+ function orderByStaleness(list, lastSurfaceOf) {
13122
+ return [...list].map((r, i) => ({ r, i, s: lastSurfaceFor(r, lastSurfaceOf) })).sort(
13123
+ (a, b) => a.s - b.s || (typeof b.r?.score === "number" ? b.r.score : 0) - (typeof a.r?.score === "number" ? a.r.score : 0) || a.i - b.i
13124
+ ).map((e) => e.r);
13125
+ }
13111
13126
  function budgetSlots(results, opts = {}) {
13112
13127
  const {
13113
13128
  thinProfile = false,
@@ -13119,7 +13134,8 @@ function budgetSlots(results, opts = {}) {
13119
13134
  interestJobPicks = [],
13120
13135
  interestJobSlots = INTEREST_JOB_SLOTS,
13121
13136
  leadRotate = process.env[LEAD_ROTATE_ENV] !== "0",
13122
- phaseMs = 0
13137
+ phaseMs = 0,
13138
+ lastSurfaceOf = null
13123
13139
  } = opts;
13124
13140
  const list = Array.isArray(results) ? results : [];
13125
13141
  const rNow = now + (Number.isFinite(phaseMs) ? phaseMs : 0);
@@ -13152,12 +13168,12 @@ function budgetSlots(results, opts = {}) {
13152
13168
  const o = Math.floor(rNow / LEAD_ROTATE_MS) % selectedHead.length;
13153
13169
  stableRoles = [...selectedHead.slice(o), ...selectedHead.slice(0, o)];
13154
13170
  }
13155
- const rotableRoles = roleMatches.slice(headPoolSize, headPoolSize + ROLE_ROTATE_WINDOW);
13156
- const rotatedRoles = rotate(rotableRoles, rNow);
13171
+ const tailRoles = roleMatches.slice(headPoolSize);
13172
+ const rotatedRoles = lastSurfaceOf ? orderByStaleness(tailRoles, lastSurfaceOf) : rotate(tailRoles.slice(0, ROLE_ROTATE_WINDOW), rNow);
13157
13173
  const roleTop = [...stableRoles, ...rotatedRoles].slice(0, roleSlots);
13158
13174
  return { roleTop, bountyTop, contributeTop, interestTop, interestJobTop };
13159
13175
  }
13160
- var TOTAL_SLOTS, BOUNTY_SLOTS, BOUNTY_MIN_MATCH, INTEREST_CONTRIBUTE_SLOTS, INTEREST_JOB_SLOTS, INTEREST_SLOT_LABEL, CONTRIBUTE_SLOTS, CONTRIBUTE_SLOTS_THIN, MIX_PRESETS, DEFAULT_MIX, ROLE_STABLE_MAX, ROLE_ROTATE_WINDOW, ROTATE_WINDOW_MS, ROLE_HEAD_POOL, ROLE_HEAD_ROTATE_MS, LEAD_ROTATE_ENV, LEAD_ROTATE_MS, ROTATE_PHASE_ENV;
13176
+ var TOTAL_SLOTS, BOUNTY_SLOTS, BOUNTY_MIN_MATCH, INTEREST_CONTRIBUTE_SLOTS, INTEREST_JOB_SLOTS, INTEREST_SLOT_LABEL, CONTRIBUTE_SLOTS, CONTRIBUTE_SLOTS_THIN, MIX_PRESETS, DEFAULT_MIX, ROLE_STABLE_MAX, ROLE_ROTATE_WINDOW, ROTATE_WINDOW_MS, ROLE_HEAD_POOL, ROLE_HEAD_ROTATE_MS, LEAD_ROTATE_ENV, LEAD_ROTATE_MS, ROTATE_PHASE_ENV, UNSEEN;
13161
13177
  var init_match_slots = __esm({
13162
13178
  "bin/match-slots.js"() {
13163
13179
  "use strict";
@@ -13179,6 +13195,7 @@ var init_match_slots = __esm({
13179
13195
  LEAD_ROTATE_ENV = "TH_LEAD_ROTATE";
13180
13196
  LEAD_ROTATE_MS = 25 * 60 * 1e3;
13181
13197
  ROTATE_PHASE_ENV = "TH_ROTATE_PHASE";
13198
+ UNSEEN = -1;
13182
13199
  }
13183
13200
  });
13184
13201
 
@@ -41450,7 +41467,7 @@ async function run26() {
41450
41467
  try {
41451
41468
  const res = await fetch(`${API_URL8}/api/index`, {
41452
41469
  signal: AbortSignal.timeout(15e3),
41453
- headers: { "Accept": "application/json" }
41470
+ headers: { Accept: "application/json" }
41454
41471
  });
41455
41472
  if (!res.ok) {
41456
41473
  process.stderr.write(`terminalhire refresh: index fetch failed (HTTP ${res.status})
@@ -41475,6 +41492,7 @@ async function run26() {
41475
41492
  let matchCount = 0;
41476
41493
  let topMatches = [];
41477
41494
  let widenReserve = [];
41495
+ let seenHistory;
41478
41496
  let cwdSignalFp;
41479
41497
  try {
41480
41498
  const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
@@ -41535,17 +41553,31 @@ async function run26() {
41535
41553
  statusMap
41536
41554
  );
41537
41555
  }
41538
- const { roleTop, bountyTop, contributeTop, interestTop, interestJobTop } = budgetSlots(results, {
41539
- thinProfile,
41540
- contributeEnabled: isContributeEnabled(),
41541
- mix: getSurfaceMix(),
41542
- interestPicks,
41543
- interestJobPicks,
41544
- // 078b: de-sync rotation phase across installs (stable per machine,
41545
- // zero persistence, zero egress). TH_ROTATE_PHASE='0' restores the
41546
- // shared global wall-clock phase (pre-078b behavior).
41547
- phaseMs: process.env[ROTATE_PHASE_ENV] !== "0" ? derivePhaseMs(hostname() + osHomedir()) : 0
41548
- });
41556
+ let lastSurfaceOf = null;
41557
+ try {
41558
+ if (process.env["TH_SEEN_SELECT"] !== "0") {
41559
+ const { loadSeenHistory: loadSeenHistory2 } = await Promise.resolve().then(() => (init_spinner_seen(), spinner_seen_exports));
41560
+ seenHistory = loadSeenHistory2();
41561
+ const entries = seenHistory.entries;
41562
+ lastSurfaceOf = (id) => entries[id]?.lastSurface;
41563
+ }
41564
+ } catch {
41565
+ }
41566
+ const { roleTop, bountyTop, contributeTop, interestTop, interestJobTop } = budgetSlots(
41567
+ results,
41568
+ {
41569
+ thinProfile,
41570
+ contributeEnabled: isContributeEnabled(),
41571
+ mix: getSurfaceMix(),
41572
+ interestPicks,
41573
+ interestJobPicks,
41574
+ lastSurfaceOf,
41575
+ // 078b: de-sync rotation phase across installs (stable per machine,
41576
+ // zero persistence, zero egress). TH_ROTATE_PHASE='0' restores the
41577
+ // shared global wall-clock phase (pre-078b behavior).
41578
+ phaseMs: process.env[ROTATE_PHASE_ENV] !== "0" ? derivePhaseMs(hostname() + osHomedir()) : 0
41579
+ }
41580
+ );
41549
41581
  const toCard = (r) => ({
41550
41582
  id: r.job.id,
41551
41583
  title: r.job.title,
@@ -41562,102 +41594,114 @@ async function run26() {
41562
41594
  // every non-interest card keeps its exact pre-037 key set.
41563
41595
  ...r.interestLabel ? { interest: r.interestLabel } : {}
41564
41596
  });
41565
- topMatches = [...roleTop, ...bountyTop, ...contributeTop, ...interestTop, ...interestJobTop].map(toCard);
41597
+ topMatches = [
41598
+ ...roleTop,
41599
+ ...bountyTop,
41600
+ ...contributeTop,
41601
+ ...interestTop,
41602
+ ...interestJobTop
41603
+ ].map(toCard);
41566
41604
  const inTop = new Set(topMatches.map((m) => m.id));
41567
41605
  widenReserve = results.filter((r) => !inTop.has(r.job.id)).slice(0, 100).map(toCard);
41568
41606
  }
41569
41607
  } catch {
41570
41608
  }
41571
41609
  let topPeers = [];
41572
- if (isPeerConnectEnabled()) try {
41573
- const { match: match2, STRONG_MATCH_THRESHOLD: STRONG_MATCH_THRESHOLD2 } = await Promise.resolve().then(() => (init_src(), src_exports));
41574
- const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
41575
- const sig = extractFingerprint2(process.cwd());
41576
- const project = readProject();
41577
- const skillTags = [
41578
- .../* @__PURE__ */ new Set([
41579
- ...Array.isArray(sig?.skillTags) ? sig.skillTags : [],
41580
- ...project && Array.isArray(project.skillTags) ? project.skillTags : []
41581
- ])
41582
- ];
41583
- if (skillTags.length > 0) {
41584
- const fp = { skillTags, prefs: project?.prefs ?? {} };
41585
- const directory = await fetchDirectory({ quiet: true });
41586
- const cards = directory?.cards ?? [];
41587
- if (cards.length > 0) {
41588
- const strongPeers = match2(fp, cards, cards.length).filter(
41589
- (r) => r.score >= STRONG_MATCH_THRESHOLD2
41590
- );
41591
- let ownLogin;
41592
- try {
41593
- const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
41594
- ownLogin = (await readProfile2())?.github?.login;
41595
- } catch {
41610
+ if (isPeerConnectEnabled())
41611
+ try {
41612
+ const { match: match2, STRONG_MATCH_THRESHOLD: STRONG_MATCH_THRESHOLD2 } = await Promise.resolve().then(() => (init_src(), src_exports));
41613
+ const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
41614
+ const sig = extractFingerprint2(process.cwd());
41615
+ const project = readProject();
41616
+ const skillTags = [
41617
+ .../* @__PURE__ */ new Set([
41618
+ ...Array.isArray(sig?.skillTags) ? sig.skillTags : [],
41619
+ ...project && Array.isArray(project.skillTags) ? project.skillTags : []
41620
+ ])
41621
+ ];
41622
+ if (skillTags.length > 0) {
41623
+ const fp = { skillTags, prefs: project?.prefs ?? {} };
41624
+ const directory = await fetchDirectory({ quiet: true });
41625
+ const cards = directory?.cards ?? [];
41626
+ if (cards.length > 0) {
41627
+ const strongPeers = match2(fp, cards, cards.length).filter(
41628
+ (r) => r.score >= STRONG_MATCH_THRESHOLD2
41629
+ );
41630
+ let ownLogin;
41631
+ try {
41632
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
41633
+ ownLogin = (await readProfile2())?.github?.login;
41634
+ } catch {
41635
+ }
41636
+ const peerResults = excludeOwnCard(strongPeers, ownLogin);
41637
+ topPeers = peerResults.map((r) => ({
41638
+ login: r.job.company,
41639
+ // public handle (person login / project owner)
41640
+ title: r.job.title,
41641
+ // public card title (name or project title)
41642
+ score: r.score,
41643
+ matchedTags: r.matchedTags,
41644
+ source: r.job.source,
41645
+ // 'person' | 'project'
41646
+ url: r.job.url,
41647
+ // public /r/<login> credential link
41648
+ id: r.job.id
41649
+ }));
41650
+ await reportMatched(peerResults);
41596
41651
  }
41597
- const peerResults = excludeOwnCard(strongPeers, ownLogin);
41598
- topPeers = peerResults.map((r) => ({
41599
- login: r.job.company,
41600
- // public handle (person login / project owner)
41601
- title: r.job.title,
41602
- // public card title (name or project title)
41603
- score: r.score,
41604
- matchedTags: r.matchedTags,
41605
- source: r.job.source,
41606
- // 'person' | 'project'
41607
- url: r.job.url,
41608
- // public /r/<login> credential link
41609
- id: r.job.id
41610
- }));
41611
- await reportMatched(peerResults);
41612
41652
  }
41653
+ } catch {
41613
41654
  }
41614
- } catch {
41615
- }
41616
41655
  let incomingPending = { count: 0 };
41617
41656
  let sessionStale = false;
41618
41657
  const sessionExpired = (res) => res.status === 401;
41619
41658
  const sessionCookie = readWebSessionFile();
41620
- if (sessionCookie && !isInboundNudgeMuted()) try {
41621
- const res = await fetch(`${API_URL8}/api/intro/list`, {
41622
- method: "GET",
41623
- headers: { Cookie: `${GH_SESSION_COOKIE8}=${sessionCookie}` },
41624
- signal: AbortSignal.timeout(1e4)
41625
- });
41626
- if (res.ok) {
41627
- const body = await res.json();
41628
- const intros = Array.isArray(body?.intros) ? body.intros : [];
41629
- const incoming = intros.filter((it) => it && it.role === "incoming" && it.status === "pending");
41630
- incomingPending = { count: incoming.length };
41631
- } else if (sessionExpired(res)) {
41632
- sessionStale = true;
41659
+ if (sessionCookie && !isInboundNudgeMuted())
41660
+ try {
41661
+ const res = await fetch(`${API_URL8}/api/intro/list`, {
41662
+ method: "GET",
41663
+ headers: { Cookie: `${GH_SESSION_COOKIE8}=${sessionCookie}` },
41664
+ signal: AbortSignal.timeout(1e4)
41665
+ });
41666
+ if (res.ok) {
41667
+ const body = await res.json();
41668
+ const intros = Array.isArray(body?.intros) ? body.intros : [];
41669
+ const incoming = intros.filter(
41670
+ (it) => it && it.role === "incoming" && it.status === "pending"
41671
+ );
41672
+ incomingPending = { count: incoming.length };
41673
+ } else if (sessionExpired(res)) {
41674
+ sessionStale = true;
41675
+ }
41676
+ } catch {
41633
41677
  }
41634
- } catch {
41635
- }
41636
41678
  let unreadChat = { count: 0 };
41637
- if (sessionCookie && !isInboundNudgeMuted()) try {
41638
- const res = await fetch(`${API_URL8}/api/chat/inbox`, {
41639
- method: "GET",
41640
- headers: { Cookie: `${GH_SESSION_COOKIE8}=${sessionCookie}` },
41641
- signal: AbortSignal.timeout(1e4)
41642
- });
41643
- if (res.ok) {
41644
- const body = await res.json();
41645
- const inbox = Array.isArray(body?.inbox) ? body.inbox : [];
41646
- const total = inbox.reduce(
41647
- (sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
41648
- 0
41649
- );
41650
- unreadChat = { count: total };
41651
- } else if (sessionExpired(res)) {
41652
- sessionStale = true;
41679
+ if (sessionCookie && !isInboundNudgeMuted())
41680
+ try {
41681
+ const res = await fetch(`${API_URL8}/api/chat/inbox`, {
41682
+ method: "GET",
41683
+ headers: { Cookie: `${GH_SESSION_COOKIE8}=${sessionCookie}` },
41684
+ signal: AbortSignal.timeout(1e4)
41685
+ });
41686
+ if (res.ok) {
41687
+ const body = await res.json();
41688
+ const inbox = Array.isArray(body?.inbox) ? body.inbox : [];
41689
+ const total = inbox.reduce(
41690
+ (sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
41691
+ 0
41692
+ );
41693
+ unreadChat = { count: total };
41694
+ } else if (sessionExpired(res)) {
41695
+ sessionStale = true;
41696
+ }
41697
+ } catch {
41653
41698
  }
41654
- } catch {
41655
- }
41656
- let seenHistory;
41657
41699
  try {
41658
- const { loadSeenHistory: loadSeenHistory2 } = await Promise.resolve().then(() => (init_spinner_seen(), spinner_seen_exports));
41659
41700
  const { filterFreshMatches: filterFreshMatches2 } = await Promise.resolve().then(() => (init_spinner(), spinner_exports));
41660
- seenHistory = loadSeenHistory2();
41701
+ if (!seenHistory) {
41702
+ const { loadSeenHistory: loadSeenHistory2 } = await Promise.resolve().then(() => (init_spinner_seen(), spinner_seen_exports));
41703
+ seenHistory = loadSeenHistory2();
41704
+ }
41661
41705
  topMatches = filterFreshMatches2(topMatches, seenHistory);
41662
41706
  } catch {
41663
41707
  }
@@ -9872,6 +9872,7 @@ __export(spinner_seen_exports, {
9872
9872
  SEEN_MAX_WIDTHS: () => SEEN_MAX_WIDTHS,
9873
9873
  SEEN_TTL_MS: () => SEEN_TTL_MS,
9874
9874
  SEEN_WINDOW_SURFACES: () => SEEN_WINDOW_SURFACES,
9875
+ isAtCapacity: () => isAtCapacity,
9875
9876
  isSuppressed: () => isSuppressed,
9876
9877
  loadSeenHistory: () => loadSeenHistory,
9877
9878
  recordSurface: () => recordSurface,
@@ -9880,6 +9881,9 @@ __export(spinner_seen_exports, {
9880
9881
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4 } from "fs";
9881
9882
  import { join as join6, dirname as dirname3 } from "path";
9882
9883
  import { homedir as homedir4 } from "os";
9884
+ function isAtCapacity(history) {
9885
+ return Object.keys(history?.entries ?? {}).length >= SEEN_MAX_ENTRIES;
9886
+ }
9883
9887
  function seenFilePath() {
9884
9888
  const dir = process.env["TERMINALHIRE_DIR"] || join6(homedir4(), ".terminalhire");
9885
9889
  return join6(dir, "seen-history.json");
@@ -9973,7 +9977,7 @@ var init_spinner_seen = __esm({
9973
9977
  init_state_dir();
9974
9978
  SEEN_WINDOW_SURFACES = 10;
9975
9979
  SEEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
9976
- SEEN_MAX_ENTRIES = 500;
9980
+ SEEN_MAX_ENTRIES = 1500;
9977
9981
  SEEN_MAX_WIDTHS = 200;
9978
9982
  }
9979
9983
  });
@@ -10518,6 +10518,7 @@ __export(spinner_seen_exports, {
10518
10518
  SEEN_MAX_WIDTHS: () => SEEN_MAX_WIDTHS,
10519
10519
  SEEN_TTL_MS: () => SEEN_TTL_MS,
10520
10520
  SEEN_WINDOW_SURFACES: () => SEEN_WINDOW_SURFACES,
10521
+ isAtCapacity: () => isAtCapacity,
10521
10522
  isSuppressed: () => isSuppressed,
10522
10523
  loadSeenHistory: () => loadSeenHistory,
10523
10524
  recordSurface: () => recordSurface,
@@ -10526,6 +10527,9 @@ __export(spinner_seen_exports, {
10526
10527
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, renameSync as renameSync5 } from "fs";
10527
10528
  import { join as join10, dirname as dirname3 } from "path";
10528
10529
  import { homedir as homedir8 } from "os";
10530
+ function isAtCapacity(history) {
10531
+ return Object.keys(history?.entries ?? {}).length >= SEEN_MAX_ENTRIES;
10532
+ }
10529
10533
  function seenFilePath() {
10530
10534
  const dir = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
10531
10535
  return join10(dir, "seen-history.json");
@@ -10619,7 +10623,7 @@ var init_spinner_seen = __esm({
10619
10623
  init_state_dir();
10620
10624
  SEEN_WINDOW_SURFACES = 10;
10621
10625
  SEEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
10622
- SEEN_MAX_ENTRIES = 500;
10626
+ SEEN_MAX_ENTRIES = 1500;
10623
10627
  SEEN_MAX_WIDTHS = 200;
10624
10628
  }
10625
10629
  });
@@ -11874,6 +11878,18 @@ function rotate(list, now, windowMs = ROTATE_WINDOW_MS) {
11874
11878
  const idx = Math.floor(now / windowMs) % list.length;
11875
11879
  return [...list.slice(idx), ...list.slice(0, idx)];
11876
11880
  }
11881
+ var UNSEEN = -1;
11882
+ function lastSurfaceFor(result, lastSurfaceOf) {
11883
+ const id = result?.job?.id;
11884
+ if (typeof id !== "string" || id.length === 0) return UNSEEN;
11885
+ const v = lastSurfaceOf(id);
11886
+ return typeof v === "number" && Number.isFinite(v) ? v : UNSEEN;
11887
+ }
11888
+ function orderByStaleness(list, lastSurfaceOf) {
11889
+ return [...list].map((r, i) => ({ r, i, s: lastSurfaceFor(r, lastSurfaceOf) })).sort(
11890
+ (a, b) => a.s - b.s || (typeof b.r?.score === "number" ? b.r.score : 0) - (typeof a.r?.score === "number" ? a.r.score : 0) || a.i - b.i
11891
+ ).map((e) => e.r);
11892
+ }
11877
11893
  function budgetSlots(results, opts = {}) {
11878
11894
  const {
11879
11895
  thinProfile = false,
@@ -11885,7 +11901,8 @@ function budgetSlots(results, opts = {}) {
11885
11901
  interestJobPicks = [],
11886
11902
  interestJobSlots = INTEREST_JOB_SLOTS,
11887
11903
  leadRotate = process.env[LEAD_ROTATE_ENV] !== "0",
11888
- phaseMs = 0
11904
+ phaseMs = 0,
11905
+ lastSurfaceOf = null
11889
11906
  } = opts;
11890
11907
  const list = Array.isArray(results) ? results : [];
11891
11908
  const rNow = now + (Number.isFinite(phaseMs) ? phaseMs : 0);
@@ -11918,8 +11935,8 @@ function budgetSlots(results, opts = {}) {
11918
11935
  const o = Math.floor(rNow / LEAD_ROTATE_MS) % selectedHead.length;
11919
11936
  stableRoles = [...selectedHead.slice(o), ...selectedHead.slice(0, o)];
11920
11937
  }
11921
- const rotableRoles = roleMatches.slice(headPoolSize, headPoolSize + ROLE_ROTATE_WINDOW);
11922
- const rotatedRoles = rotate(rotableRoles, rNow);
11938
+ const tailRoles = roleMatches.slice(headPoolSize);
11939
+ const rotatedRoles = lastSurfaceOf ? orderByStaleness(tailRoles, lastSurfaceOf) : rotate(tailRoles.slice(0, ROLE_ROTATE_WINDOW), rNow);
11923
11940
  const roleTop = [...stableRoles, ...rotatedRoles].slice(0, roleSlots);
11924
11941
  return { roleTop, bountyTop, contributeTop, interestTop, interestJobTop };
11925
11942
  }
@@ -12019,7 +12036,7 @@ async function run() {
12019
12036
  try {
12020
12037
  const res = await fetch(`${API_URL2}/api/index`, {
12021
12038
  signal: AbortSignal.timeout(15e3),
12022
- headers: { "Accept": "application/json" }
12039
+ headers: { Accept: "application/json" }
12023
12040
  });
12024
12041
  if (!res.ok) {
12025
12042
  process.stderr.write(`terminalhire refresh: index fetch failed (HTTP ${res.status})
@@ -12044,6 +12061,7 @@ async function run() {
12044
12061
  let matchCount = 0;
12045
12062
  let topMatches = [];
12046
12063
  let widenReserve = [];
12064
+ let seenHistory;
12047
12065
  let cwdSignalFp;
12048
12066
  try {
12049
12067
  const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
@@ -12104,17 +12122,31 @@ async function run() {
12104
12122
  statusMap
12105
12123
  );
12106
12124
  }
12107
- const { roleTop, bountyTop, contributeTop, interestTop, interestJobTop } = budgetSlots(results, {
12108
- thinProfile,
12109
- contributeEnabled: isContributeEnabled(),
12110
- mix: getSurfaceMix(),
12111
- interestPicks,
12112
- interestJobPicks,
12113
- // 078b: de-sync rotation phase across installs (stable per machine,
12114
- // zero persistence, zero egress). TH_ROTATE_PHASE='0' restores the
12115
- // shared global wall-clock phase (pre-078b behavior).
12116
- phaseMs: process.env[ROTATE_PHASE_ENV] !== "0" ? derivePhaseMs(hostname() + osHomedir()) : 0
12117
- });
12125
+ let lastSurfaceOf = null;
12126
+ try {
12127
+ if (process.env["TH_SEEN_SELECT"] !== "0") {
12128
+ const { loadSeenHistory: loadSeenHistory2 } = await Promise.resolve().then(() => (init_spinner_seen(), spinner_seen_exports));
12129
+ seenHistory = loadSeenHistory2();
12130
+ const entries = seenHistory.entries;
12131
+ lastSurfaceOf = (id) => entries[id]?.lastSurface;
12132
+ }
12133
+ } catch {
12134
+ }
12135
+ const { roleTop, bountyTop, contributeTop, interestTop, interestJobTop } = budgetSlots(
12136
+ results,
12137
+ {
12138
+ thinProfile,
12139
+ contributeEnabled: isContributeEnabled(),
12140
+ mix: getSurfaceMix(),
12141
+ interestPicks,
12142
+ interestJobPicks,
12143
+ lastSurfaceOf,
12144
+ // 078b: de-sync rotation phase across installs (stable per machine,
12145
+ // zero persistence, zero egress). TH_ROTATE_PHASE='0' restores the
12146
+ // shared global wall-clock phase (pre-078b behavior).
12147
+ phaseMs: process.env[ROTATE_PHASE_ENV] !== "0" ? derivePhaseMs(hostname() + osHomedir()) : 0
12148
+ }
12149
+ );
12118
12150
  const toCard = (r) => ({
12119
12151
  id: r.job.id,
12120
12152
  title: r.job.title,
@@ -12131,102 +12163,114 @@ async function run() {
12131
12163
  // every non-interest card keeps its exact pre-037 key set.
12132
12164
  ...r.interestLabel ? { interest: r.interestLabel } : {}
12133
12165
  });
12134
- topMatches = [...roleTop, ...bountyTop, ...contributeTop, ...interestTop, ...interestJobTop].map(toCard);
12166
+ topMatches = [
12167
+ ...roleTop,
12168
+ ...bountyTop,
12169
+ ...contributeTop,
12170
+ ...interestTop,
12171
+ ...interestJobTop
12172
+ ].map(toCard);
12135
12173
  const inTop = new Set(topMatches.map((m) => m.id));
12136
12174
  widenReserve = results.filter((r) => !inTop.has(r.job.id)).slice(0, 100).map(toCard);
12137
12175
  }
12138
12176
  } catch {
12139
12177
  }
12140
12178
  let topPeers = [];
12141
- if (isPeerConnectEnabled()) try {
12142
- const { match: match2, STRONG_MATCH_THRESHOLD: STRONG_MATCH_THRESHOLD2 } = await Promise.resolve().then(() => (init_src(), src_exports));
12143
- const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
12144
- const sig = extractFingerprint2(process.cwd());
12145
- const project = readProject();
12146
- const skillTags = [
12147
- .../* @__PURE__ */ new Set([
12148
- ...Array.isArray(sig?.skillTags) ? sig.skillTags : [],
12149
- ...project && Array.isArray(project.skillTags) ? project.skillTags : []
12150
- ])
12151
- ];
12152
- if (skillTags.length > 0) {
12153
- const fp = { skillTags, prefs: project?.prefs ?? {} };
12154
- const directory = await fetchDirectory({ quiet: true });
12155
- const cards = directory?.cards ?? [];
12156
- if (cards.length > 0) {
12157
- const strongPeers = match2(fp, cards, cards.length).filter(
12158
- (r) => r.score >= STRONG_MATCH_THRESHOLD2
12159
- );
12160
- let ownLogin;
12161
- try {
12162
- const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
12163
- ownLogin = (await readProfile2())?.github?.login;
12164
- } catch {
12179
+ if (isPeerConnectEnabled())
12180
+ try {
12181
+ const { match: match2, STRONG_MATCH_THRESHOLD: STRONG_MATCH_THRESHOLD2 } = await Promise.resolve().then(() => (init_src(), src_exports));
12182
+ const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
12183
+ const sig = extractFingerprint2(process.cwd());
12184
+ const project = readProject();
12185
+ const skillTags = [
12186
+ .../* @__PURE__ */ new Set([
12187
+ ...Array.isArray(sig?.skillTags) ? sig.skillTags : [],
12188
+ ...project && Array.isArray(project.skillTags) ? project.skillTags : []
12189
+ ])
12190
+ ];
12191
+ if (skillTags.length > 0) {
12192
+ const fp = { skillTags, prefs: project?.prefs ?? {} };
12193
+ const directory = await fetchDirectory({ quiet: true });
12194
+ const cards = directory?.cards ?? [];
12195
+ if (cards.length > 0) {
12196
+ const strongPeers = match2(fp, cards, cards.length).filter(
12197
+ (r) => r.score >= STRONG_MATCH_THRESHOLD2
12198
+ );
12199
+ let ownLogin;
12200
+ try {
12201
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
12202
+ ownLogin = (await readProfile2())?.github?.login;
12203
+ } catch {
12204
+ }
12205
+ const peerResults = excludeOwnCard(strongPeers, ownLogin);
12206
+ topPeers = peerResults.map((r) => ({
12207
+ login: r.job.company,
12208
+ // public handle (person login / project owner)
12209
+ title: r.job.title,
12210
+ // public card title (name or project title)
12211
+ score: r.score,
12212
+ matchedTags: r.matchedTags,
12213
+ source: r.job.source,
12214
+ // 'person' | 'project'
12215
+ url: r.job.url,
12216
+ // public /r/<login> credential link
12217
+ id: r.job.id
12218
+ }));
12219
+ await reportMatched(peerResults);
12165
12220
  }
12166
- const peerResults = excludeOwnCard(strongPeers, ownLogin);
12167
- topPeers = peerResults.map((r) => ({
12168
- login: r.job.company,
12169
- // public handle (person login / project owner)
12170
- title: r.job.title,
12171
- // public card title (name or project title)
12172
- score: r.score,
12173
- matchedTags: r.matchedTags,
12174
- source: r.job.source,
12175
- // 'person' | 'project'
12176
- url: r.job.url,
12177
- // public /r/<login> credential link
12178
- id: r.job.id
12179
- }));
12180
- await reportMatched(peerResults);
12181
12221
  }
12222
+ } catch {
12182
12223
  }
12183
- } catch {
12184
- }
12185
12224
  let incomingPending = { count: 0 };
12186
12225
  let sessionStale = false;
12187
12226
  const sessionExpired = (res) => res.status === 401;
12188
12227
  const sessionCookie = readWebSessionFile();
12189
- if (sessionCookie && !isInboundNudgeMuted()) try {
12190
- const res = await fetch(`${API_URL2}/api/intro/list`, {
12191
- method: "GET",
12192
- headers: { Cookie: `${GH_SESSION_COOKIE}=${sessionCookie}` },
12193
- signal: AbortSignal.timeout(1e4)
12194
- });
12195
- if (res.ok) {
12196
- const body = await res.json();
12197
- const intros = Array.isArray(body?.intros) ? body.intros : [];
12198
- const incoming = intros.filter((it) => it && it.role === "incoming" && it.status === "pending");
12199
- incomingPending = { count: incoming.length };
12200
- } else if (sessionExpired(res)) {
12201
- sessionStale = true;
12228
+ if (sessionCookie && !isInboundNudgeMuted())
12229
+ try {
12230
+ const res = await fetch(`${API_URL2}/api/intro/list`, {
12231
+ method: "GET",
12232
+ headers: { Cookie: `${GH_SESSION_COOKIE}=${sessionCookie}` },
12233
+ signal: AbortSignal.timeout(1e4)
12234
+ });
12235
+ if (res.ok) {
12236
+ const body = await res.json();
12237
+ const intros = Array.isArray(body?.intros) ? body.intros : [];
12238
+ const incoming = intros.filter(
12239
+ (it) => it && it.role === "incoming" && it.status === "pending"
12240
+ );
12241
+ incomingPending = { count: incoming.length };
12242
+ } else if (sessionExpired(res)) {
12243
+ sessionStale = true;
12244
+ }
12245
+ } catch {
12202
12246
  }
12203
- } catch {
12204
- }
12205
12247
  let unreadChat = { count: 0 };
12206
- if (sessionCookie && !isInboundNudgeMuted()) try {
12207
- const res = await fetch(`${API_URL2}/api/chat/inbox`, {
12208
- method: "GET",
12209
- headers: { Cookie: `${GH_SESSION_COOKIE}=${sessionCookie}` },
12210
- signal: AbortSignal.timeout(1e4)
12211
- });
12212
- if (res.ok) {
12213
- const body = await res.json();
12214
- const inbox = Array.isArray(body?.inbox) ? body.inbox : [];
12215
- const total = inbox.reduce(
12216
- (sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
12217
- 0
12218
- );
12219
- unreadChat = { count: total };
12220
- } else if (sessionExpired(res)) {
12221
- sessionStale = true;
12248
+ if (sessionCookie && !isInboundNudgeMuted())
12249
+ try {
12250
+ const res = await fetch(`${API_URL2}/api/chat/inbox`, {
12251
+ method: "GET",
12252
+ headers: { Cookie: `${GH_SESSION_COOKIE}=${sessionCookie}` },
12253
+ signal: AbortSignal.timeout(1e4)
12254
+ });
12255
+ if (res.ok) {
12256
+ const body = await res.json();
12257
+ const inbox = Array.isArray(body?.inbox) ? body.inbox : [];
12258
+ const total = inbox.reduce(
12259
+ (sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
12260
+ 0
12261
+ );
12262
+ unreadChat = { count: total };
12263
+ } else if (sessionExpired(res)) {
12264
+ sessionStale = true;
12265
+ }
12266
+ } catch {
12222
12267
  }
12223
- } catch {
12224
- }
12225
- let seenHistory;
12226
12268
  try {
12227
- const { loadSeenHistory: loadSeenHistory2 } = await Promise.resolve().then(() => (init_spinner_seen(), spinner_seen_exports));
12228
12269
  const { filterFreshMatches: filterFreshMatches2 } = await Promise.resolve().then(() => (init_spinner(), spinner_exports));
12229
- seenHistory = loadSeenHistory2();
12270
+ if (!seenHistory) {
12271
+ const { loadSeenHistory: loadSeenHistory2 } = await Promise.resolve().then(() => (init_spinner_seen(), spinner_seen_exports));
12272
+ seenHistory = loadSeenHistory2();
12273
+ }
12230
12274
  topMatches = filterFreshMatches2(topMatches, seenHistory);
12231
12275
  } catch {
12232
12276
  }
package/dist/bin/jpi.js CHANGED
@@ -73,8 +73,11 @@ function readStdinSync() {
73
73
  sock.unref();
74
74
  } catch {
75
75
  }
76
- const DEADLINE_MS = 200;
76
+ const IDLE_MS = 200;
77
+ const STREAM_IDLE_MS = 500;
78
+ const MAX_TOTAL_MS = 1500;
77
79
  const start = Date.now();
80
+ let lastProgress = start;
78
81
  const idle = new Int32Array(new SharedArrayBuffer(4));
79
82
  const chunks = [];
80
83
  const buf = Buffer.alloc(1 << 16);
@@ -85,7 +88,9 @@ function readStdinSync() {
85
88
  n = readSync(0, buf, 0, buf.length, null);
86
89
  } catch (e) {
87
90
  if (e && e.code === "EAGAIN") {
88
- if (Date.now() - start > DEADLINE_MS) break;
91
+ const idleBudget = chunks.length > 0 ? STREAM_IDLE_MS : IDLE_MS;
92
+ if (Date.now() - lastProgress > idleBudget) break;
93
+ if (Date.now() - start > MAX_TOTAL_MS) break;
89
94
  Atomics.wait(idle, 0, 0, 5);
90
95
  continue;
91
96
  }
@@ -93,7 +98,8 @@ function readStdinSync() {
93
98
  }
94
99
  if (n === 0) break;
95
100
  chunks.push(Buffer.from(buf.subarray(0, n)));
96
- if (Date.now() - start > DEADLINE_MS) break;
101
+ lastProgress = Date.now();
102
+ if (Date.now() - start > MAX_TOTAL_MS) break;
97
103
  }
98
104
  } catch {
99
105
  return {};
@@ -112,10 +118,12 @@ function readStdinWin32() {
112
118
  const chunks = [];
113
119
  let settled = false;
114
120
  let timer;
121
+ let hardStop;
115
122
  const finish = () => {
116
123
  if (settled) return;
117
124
  settled = true;
118
125
  clearTimeout(timer);
126
+ clearTimeout(hardStop);
119
127
  try {
120
128
  process.stdin.pause();
121
129
  } catch {
@@ -128,9 +136,22 @@ function readStdinWin32() {
128
136
  resolve({});
129
137
  }
130
138
  };
131
- timer = setTimeout(finish, 200);
132
- if (typeof timer.unref === "function") timer.unref();
133
- process.stdin.on("data", (c) => chunks.push(c));
139
+ const IDLE_MS = 200;
140
+ const STREAM_IDLE_MS = 500;
141
+ const MAX_TOTAL_MS = 1500;
142
+ const started = Date.now();
143
+ const arm = (ms) => {
144
+ clearTimeout(timer);
145
+ timer = setTimeout(finish, ms);
146
+ if (typeof timer.unref === "function") timer.unref();
147
+ };
148
+ arm(IDLE_MS);
149
+ hardStop = setTimeout(finish, MAX_TOTAL_MS);
150
+ if (typeof hardStop.unref === "function") hardStop.unref();
151
+ process.stdin.on("data", (c) => {
152
+ chunks.push(c);
153
+ arm(Math.min(STREAM_IDLE_MS, Math.max(0, MAX_TOTAL_MS - (Date.now() - started))));
154
+ });
134
155
  process.stdin.on("end", finish);
135
156
  process.stdin.on("error", finish);
136
157
  });
@@ -54,7 +54,7 @@ function ensureStateDir(dir) {
54
54
  // bin/spinner-seen.js
55
55
  var SEEN_WINDOW_SURFACES = 10;
56
56
  var SEEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
57
- var SEEN_MAX_ENTRIES = 500;
57
+ var SEEN_MAX_ENTRIES = 1500;
58
58
  var SEEN_MAX_WIDTHS = 200;
59
59
  function seenFilePath() {
60
60
  const dir = process.env["TERMINALHIRE_DIR"] || join(homedir(), ".terminalhire");
@@ -54,7 +54,10 @@ function ensureStateDir(dir) {
54
54
  // bin/spinner-seen.js
55
55
  var SEEN_WINDOW_SURFACES = 10;
56
56
  var SEEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
57
- var SEEN_MAX_ENTRIES = 500;
57
+ var SEEN_MAX_ENTRIES = 1500;
58
+ function isAtCapacity(history) {
59
+ return Object.keys(history?.entries ?? {}).length >= SEEN_MAX_ENTRIES;
60
+ }
58
61
  var SEEN_MAX_WIDTHS = 200;
59
62
  function seenFilePath() {
60
63
  const dir = process.env["TERMINALHIRE_DIR"] || join(homedir(), ".terminalhire");
@@ -147,6 +150,7 @@ export {
147
150
  SEEN_MAX_WIDTHS,
148
151
  SEEN_TTL_MS,
149
152
  SEEN_WINDOW_SURFACES,
153
+ isAtCapacity,
150
154
  isSuppressed,
151
155
  loadSeenHistory,
152
156
  recordSurface,
@@ -202,7 +202,7 @@ import { join as join3, dirname as dirname2 } from "path";
202
202
  import { homedir as homedir2 } from "os";
203
203
  var SEEN_WINDOW_SURFACES = 10;
204
204
  var SEEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
205
- var SEEN_MAX_ENTRIES = 500;
205
+ var SEEN_MAX_ENTRIES = 1500;
206
206
  var SEEN_MAX_WIDTHS = 200;
207
207
  function seenFilePath() {
208
208
  const dir = process.env["TERMINALHIRE_DIR"] || join3(homedir2(), ".terminalhire");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Your profile never leaves your machine.",
5
5
  "repository": {
6
6
  "type": "git",