@hua-labs/tap 0.3.1 → 0.4.1

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.
@@ -6807,6 +6807,88 @@ var require_dist = __commonJS({
6807
6807
  }
6808
6808
  });
6809
6809
 
6810
+ // ../tap-plugin/channels/tap-identity.ts
6811
+ function trimAddress(value) {
6812
+ return value?.trim() ?? "";
6813
+ }
6814
+ function canonicalizeAgentId(value) {
6815
+ return trimAddress(value).replace(/-/g, "_");
6816
+ }
6817
+ function isBroadcastRecipient(value) {
6818
+ return BROADCAST_RECIPIENTS.has(trimAddress(value));
6819
+ }
6820
+ function isPlaceholderAgentValue(value) {
6821
+ const normalized = trimAddress(value);
6822
+ return !normalized || PLACEHOLDER_AGENT_VALUES.has(normalized);
6823
+ }
6824
+ function sameRoutingAddress(left, right) {
6825
+ const normalizedLeft = trimAddress(left);
6826
+ const normalizedRight = trimAddress(right);
6827
+ if (!normalizedLeft || !normalizedRight) {
6828
+ return false;
6829
+ }
6830
+ if (isBroadcastRecipient(normalizedLeft) && isBroadcastRecipient(normalizedRight)) {
6831
+ return true;
6832
+ }
6833
+ return normalizedLeft === normalizedRight || canonicalizeAgentId(normalizedLeft) === canonicalizeAgentId(normalizedRight);
6834
+ }
6835
+ function matchesAgentRecipient(recipient, agentId, agentName) {
6836
+ const normalizedRecipient = trimAddress(recipient);
6837
+ if (!normalizedRecipient) {
6838
+ return false;
6839
+ }
6840
+ return isBroadcastRecipient(normalizedRecipient) || sameRoutingAddress(normalizedRecipient, agentId) || normalizedRecipient === trimAddress(agentName);
6841
+ }
6842
+ function isOwnMessageAddress(sender, agentId, agentName) {
6843
+ const normalizedSender = trimAddress(sender);
6844
+ if (!normalizedSender) {
6845
+ return false;
6846
+ }
6847
+ return sameRoutingAddress(normalizedSender, agentId) || normalizedSender === trimAddress(agentName);
6848
+ }
6849
+ function normalizeRecipientList(rawRecipients, exclude = []) {
6850
+ let recipients;
6851
+ if (rawRecipients == null) {
6852
+ recipients = void 0;
6853
+ } else if (typeof rawRecipients === "string") {
6854
+ const trimmed = trimAddress(rawRecipients);
6855
+ recipients = trimmed ? [trimmed] : void 0;
6856
+ } else if (Array.isArray(rawRecipients)) {
6857
+ const valid = rawRecipients.filter(
6858
+ (value) => typeof value === "string" && trimAddress(value).length > 0
6859
+ ).map((value) => trimAddress(value));
6860
+ recipients = valid.length > 0 ? valid : void 0;
6861
+ } else {
6862
+ recipients = void 0;
6863
+ }
6864
+ if (!recipients) {
6865
+ return void 0;
6866
+ }
6867
+ const filtered = [];
6868
+ for (const recipient of recipients) {
6869
+ if (exclude.some((value) => sameRoutingAddress(value, recipient))) {
6870
+ continue;
6871
+ }
6872
+ if (filtered.some((value) => sameRoutingAddress(value, recipient))) {
6873
+ continue;
6874
+ }
6875
+ filtered.push(recipient);
6876
+ }
6877
+ return filtered.length > 0 ? filtered : void 0;
6878
+ }
6879
+ var BROADCAST_RECIPIENTS, PLACEHOLDER_AGENT_VALUES;
6880
+ var init_tap_identity = __esm({
6881
+ "../tap-plugin/channels/tap-identity.ts"() {
6882
+ "use strict";
6883
+ BROADCAST_RECIPIENTS = /* @__PURE__ */ new Set(["\uC804\uCCB4", "all"]);
6884
+ PLACEHOLDER_AGENT_VALUES = /* @__PURE__ */ new Set([
6885
+ "unknown",
6886
+ "unnamed",
6887
+ "<set-per-session>"
6888
+ ]);
6889
+ }
6890
+ });
6891
+
6810
6892
  // ../tap-plugin/channels/tap-utils.ts
6811
6893
  var tap_utils_exports = {};
6812
6894
  __export(tap_utils_exports, {
@@ -6822,6 +6904,7 @@ __export(tap_utils_exports, {
6822
6904
  RECEIPTS_PATH: () => RECEIPTS_PATH,
6823
6905
  REVIEWS_DIR: () => REVIEWS_DIR,
6824
6906
  SERVER_START: () => SERVER_START,
6907
+ canonicalizeAgentId: () => canonicalizeAgentId2,
6825
6908
  debug: () => debug,
6826
6909
  getAgentId: () => getAgentId,
6827
6910
  getAgentName: () => getAgentName,
@@ -6835,30 +6918,63 @@ __export(tap_utils_exports, {
6835
6918
  isNameConfirmed: () => isNameConfirmed,
6836
6919
  normalizeSources: () => normalizeSources,
6837
6920
  parseFilename: () => parseFilename,
6921
+ parseFrontmatter: () => parseFrontmatter,
6922
+ parseMessageRoute: () => parseMessageRoute,
6838
6923
  setAgentName: () => setAgentName,
6839
6924
  stripBom: () => stripBom,
6925
+ stripFrontmatter: () => stripFrontmatter,
6840
6926
  updateActivityTime: () => updateActivityTime
6841
6927
  });
6842
6928
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
6843
6929
  import { join, resolve } from "path";
6844
- function resolveInitialId() {
6845
- const envId = process.env.TAP_AGENT_ID;
6846
- if (envId && !PLACEHOLDER_NAMES.has(envId)) return envId.replace(/-/g, "_");
6847
- const envName = process.env.TAP_AGENT_NAME;
6848
- if (envName && !PLACEHOLDER_NAMES.has(envName))
6849
- return envName.replace(/-/g, "_");
6850
- return "unknown";
6930
+ function isConcreteIdentity(value) {
6931
+ return !isPlaceholderAgentValue(value);
6932
+ }
6933
+ function normalizeAgentId(value) {
6934
+ return canonicalizeAgentId(value);
6851
6935
  }
6852
- function resolveNameFromState() {
6936
+ function loadStateInstances() {
6853
6937
  const stateDir = process.env.TAP_STATE_DIR;
6854
- const agentId = process.env.TAP_AGENT_ID;
6855
- if (!stateDir || !agentId) return null;
6938
+ if (!stateDir) return null;
6856
6939
  try {
6857
6940
  const statePath = join(stateDir, "state.json");
6858
6941
  if (!existsSync(statePath)) return null;
6859
6942
  const state = JSON.parse(readFileSync(statePath, "utf-8"));
6860
- const instance = state.instances?.[agentId] ?? state.instances?.[agentId.replace(/_/g, "-")];
6861
- return instance?.agentName ?? null;
6943
+ return state.instances ?? null;
6944
+ } catch {
6945
+ return null;
6946
+ }
6947
+ }
6948
+ function resolveSingleCodexBootstrap() {
6949
+ const instances = loadStateInstances();
6950
+ if (!instances) return null;
6951
+ const installedCodexInstances = Object.entries(instances).filter(
6952
+ ([, instance2]) => instance2?.runtime === "codex" && instance2?.installed
6953
+ );
6954
+ if (installedCodexInstances.length !== 1) return null;
6955
+ const [instanceId, instance] = installedCodexInstances[0];
6956
+ return {
6957
+ agentId: normalizeAgentId(instanceId),
6958
+ agentName: typeof instance.agentName === "string" && !isPlaceholderAgentValue(instance.agentName) ? instance.agentName : null
6959
+ };
6960
+ }
6961
+ function resolveInitialId(stateBootstrap2) {
6962
+ const envId = process.env.TAP_AGENT_ID;
6963
+ if (isConcreteIdentity(envId)) return normalizeAgentId(envId);
6964
+ const envName = process.env.TAP_AGENT_NAME;
6965
+ if (isConcreteIdentity(envName)) return normalizeAgentId(envName);
6966
+ return stateBootstrap2?.agentId ?? "unknown";
6967
+ }
6968
+ function resolveNameFromState(agentId, stateBootstrap2) {
6969
+ if (agentId === "unknown") return null;
6970
+ if (stateBootstrap2?.agentId === agentId && stateBootstrap2.agentName) {
6971
+ return stateBootstrap2.agentName;
6972
+ }
6973
+ try {
6974
+ const instances = loadStateInstances();
6975
+ if (!instances) return null;
6976
+ const instance = instances[agentId] ?? instances[agentId.replace(/_/g, "-")];
6977
+ return typeof instance?.agentName === "string" && !isPlaceholderAgentValue(instance.agentName) ? instance.agentName : null;
6862
6978
  } catch {
6863
6979
  return null;
6864
6980
  }
@@ -6876,7 +6992,7 @@ function setAgentName(name) {
6876
6992
  _agentName = name;
6877
6993
  _nameConfirmed = true;
6878
6994
  if (!_idLocked) {
6879
- _agentId = name.replace(/-/g, "_");
6995
+ _agentId = canonicalizeAgentId(name);
6880
6996
  _idLocked = true;
6881
6997
  }
6882
6998
  }
@@ -6895,6 +7011,35 @@ function debug(message) {
6895
7011
  function stripBom(text) {
6896
7012
  return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
6897
7013
  }
7014
+ function parseFrontmatter(content) {
7015
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
7016
+ if (!match) return null;
7017
+ const fields = {};
7018
+ for (const line of match[1].split("\n")) {
7019
+ const kv = line.match(/^(\w+):\s*(.+)$/);
7020
+ if (kv) fields[kv[1]] = kv[2].trim();
7021
+ }
7022
+ if (!fields.from || !fields.to) return null;
7023
+ return {
7024
+ from: fields.from,
7025
+ from_name: fields.from_name,
7026
+ to: fields.to,
7027
+ to_name: fields.to_name,
7028
+ subject: fields.subject ?? "",
7029
+ sent_at: fields.sent_at,
7030
+ type: fields.type
7031
+ };
7032
+ }
7033
+ function stripFrontmatter(content) {
7034
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, "");
7035
+ }
7036
+ function parseMessageRoute(filename, content) {
7037
+ if (content) {
7038
+ const fm = parseFrontmatter(content);
7039
+ if (fm) return { from: fm.from, to: fm.to, subject: fm.subject };
7040
+ }
7041
+ return parseFilename(filename);
7042
+ }
6898
7043
  function parseFilename(filename) {
6899
7044
  const withoutExt = filename.replace(/\.md$/, "");
6900
7045
  const dateMatch = withoutExt.match(/^(\d{8})-(.+)$/);
@@ -6916,8 +7061,11 @@ function parseFilename(filename) {
6916
7061
  }
6917
7062
  return null;
6918
7063
  }
7064
+ function canonicalizeAgentId2(id) {
7065
+ return canonicalizeAgentId(id);
7066
+ }
6919
7067
  function isForMe(to) {
6920
- return to === _agentId || to === _agentName || to === "\uC804\uCCB4" || to === "all";
7068
+ return matchesAgentRecipient(to, _agentId, _agentName);
6921
7069
  }
6922
7070
  function normalizeSources(value) {
6923
7071
  if (!Array.isArray(value) || value.length === 0) {
@@ -6959,10 +7107,11 @@ function getRecentSenders() {
6959
7107
  }
6960
7108
  return senders;
6961
7109
  }
6962
- var RAW_COMMS_DIR, COMMS_DIR, INBOX_DIR, REVIEWS_DIR, FINDINGS_DIR, RECEIPTS_DIR, RECEIPTS_PATH, RECEIPTS_LOCK, HEARTBEATS_PATH, HEARTBEATS_LOCK, ARCHIVE_DIR, DB_PATH, SERVER_START, PLACEHOLDER_NAMES, _agentId, _agentName, _idLocked, _nameConfirmed, _lastActivityTime;
7110
+ var RAW_COMMS_DIR, COMMS_DIR, INBOX_DIR, REVIEWS_DIR, FINDINGS_DIR, RECEIPTS_DIR, RECEIPTS_PATH, RECEIPTS_LOCK, HEARTBEATS_PATH, HEARTBEATS_LOCK, ARCHIVE_DIR, DB_PATH, SERVER_START, stateBootstrap, _agentId, _agentName, _idLocked, _nameConfirmed, _lastActivityTime;
6963
7111
  var init_tap_utils = __esm({
6964
7112
  "../tap-plugin/channels/tap-utils.ts"() {
6965
7113
  "use strict";
7114
+ init_tap_identity();
6966
7115
  RAW_COMMS_DIR = process.env.TAP_COMMS_DIR;
6967
7116
  if (!RAW_COMMS_DIR) {
6968
7117
  console.error(
@@ -6982,11 +7131,11 @@ var init_tap_utils = __esm({
6982
7131
  ARCHIVE_DIR = join(COMMS_DIR, "archive");
6983
7132
  DB_PATH = join(COMMS_DIR, "tap.db");
6984
7133
  SERVER_START = Date.now();
6985
- PLACEHOLDER_NAMES = /* @__PURE__ */ new Set(["unknown", "unnamed", "<set-per-session>"]);
6986
- _agentId = resolveInitialId();
6987
- _agentName = resolveNameFromState() ?? (process.env.TAP_AGENT_NAME && !PLACEHOLDER_NAMES.has(process.env.TAP_AGENT_NAME) ? process.env.TAP_AGENT_NAME : "unknown");
7134
+ stateBootstrap = resolveSingleCodexBootstrap();
7135
+ _agentId = resolveInitialId(stateBootstrap);
7136
+ _agentName = resolveNameFromState(_agentId, stateBootstrap) ?? (isConcreteIdentity(process.env.TAP_AGENT_NAME) ? process.env.TAP_AGENT_NAME : "unknown");
6988
7137
  _idLocked = _agentId !== "unknown";
6989
- _nameConfirmed = _agentName !== "unknown" && !PLACEHOLDER_NAMES.has(_agentName);
7138
+ _nameConfirmed = !isPlaceholderAgentValue(_agentName);
6990
7139
  _lastActivityTime = (/* @__PURE__ */ new Date()).toISOString();
6991
7140
  }
6992
7141
  });
@@ -7018,7 +7167,37 @@ import {
7018
7167
  unlinkSync,
7019
7168
  writeFileSync
7020
7169
  } from "fs";
7170
+ import { createHash } from "crypto";
7021
7171
  import { join as join2 } from "path";
7172
+ function getBridgeProcessedDirs() {
7173
+ const now = Date.now();
7174
+ if (now - _bridgeDirsCachedAt < BRIDGE_DIR_CACHE_TTL_MS) {
7175
+ return _bridgeProcessedDirs;
7176
+ }
7177
+ _bridgeDirsCachedAt = now;
7178
+ if (!REPO_ROOT) {
7179
+ _bridgeProcessedDirs = [];
7180
+ return _bridgeProcessedDirs;
7181
+ }
7182
+ const tmpDir = join2(REPO_ROOT, ".tmp");
7183
+ if (!existsSync2(tmpDir)) {
7184
+ _bridgeProcessedDirs = [];
7185
+ return _bridgeProcessedDirs;
7186
+ }
7187
+ try {
7188
+ _bridgeProcessedDirs = readdirSync2(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge")).map((d) => join2(tmpDir, d, "processed")).filter((p) => existsSync2(p));
7189
+ } catch {
7190
+ _bridgeProcessedDirs = [];
7191
+ }
7192
+ return _bridgeProcessedDirs;
7193
+ }
7194
+ function isBridgeProcessed(filePath, mtimeMs) {
7195
+ const dirs = getBridgeProcessedDirs();
7196
+ if (dirs.length === 0) return false;
7197
+ const markerId = createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
7198
+ const markerFile = `${markerId}.done`;
7199
+ return dirs.some((dir) => existsSync2(join2(dir, markerFile)));
7200
+ }
7022
7201
  function acquireLock(lockPath, retries = 3, delayMs = 100) {
7023
7202
  for (let attempt = 0; attempt < retries; attempt++) {
7024
7203
  try {
@@ -7148,6 +7327,10 @@ function getUnreadItems(options) {
7148
7327
  continue;
7149
7328
  }
7150
7329
  if (effectiveSinceMs && mtime < effectiveSinceMs) continue;
7330
+ if (isBridgeProcessed(fullPath, mtime)) {
7331
+ readFiles.add(key);
7332
+ continue;
7333
+ }
7151
7334
  let content;
7152
7335
  try {
7153
7336
  content = stripBom(readFileSync2(fullPath, "utf-8"));
@@ -7158,13 +7341,17 @@ function getUnreadItems(options) {
7158
7341
  let to = "all";
7159
7342
  let subject = filename.replace(/\.md$/, "");
7160
7343
  if (source === "inbox") {
7161
- const parsed = parseFilename(filename);
7344
+ const fm = parseFrontmatter(content);
7345
+ const parsed = fm ? { from: fm.from, to: fm.to, subject: fm.subject } : parseFilename(filename);
7162
7346
  if (!parsed || !isForMe(parsed.to)) continue;
7163
- if (parsed.from === getAgentId() || parsed.from === getAgentName())
7347
+ if (isOwnMessageAddress(parsed.from, getAgentId(), getAgentName()))
7164
7348
  continue;
7165
- from = resolveAgentLabel(parsed.from, heartbeatStore);
7166
- to = resolveAgentLabel(parsed.to, heartbeatStore);
7349
+ from = resolveAgentLabel(fm?.from_name ?? parsed.from, heartbeatStore);
7350
+ to = resolveAgentLabel(fm?.to_name ?? parsed.to, heartbeatStore);
7167
7351
  subject = parsed.subject;
7352
+ if (fm && includeContent) {
7353
+ content = stripFrontmatter(content);
7354
+ }
7168
7355
  }
7169
7356
  const item = {
7170
7357
  source,
@@ -7189,13 +7376,18 @@ function getUnreadItems(options) {
7189
7376
  }
7190
7377
  return items;
7191
7378
  }
7192
- var startupFiles, readFiles;
7379
+ var startupFiles, readFiles, REPO_ROOT, BRIDGE_DIR_CACHE_TTL_MS, _bridgeProcessedDirs, _bridgeDirsCachedAt;
7193
7380
  var init_tap_io = __esm({
7194
7381
  "../tap-plugin/channels/tap-io.ts"() {
7195
7382
  "use strict";
7196
7383
  init_tap_utils();
7384
+ init_tap_identity();
7197
7385
  startupFiles = /* @__PURE__ */ new Set();
7198
7386
  readFiles = /* @__PURE__ */ new Set();
7387
+ REPO_ROOT = process.env.TAP_REPO_ROOT ?? null;
7388
+ BRIDGE_DIR_CACHE_TTL_MS = 3e4;
7389
+ _bridgeProcessedDirs = [];
7390
+ _bridgeDirsCachedAt = 0;
7199
7391
  }
7200
7392
  });
7201
7393
 
@@ -21209,6 +21401,7 @@ var StdioServerTransport = class {
21209
21401
  };
21210
21402
 
21211
21403
  // ../tap-plugin/channels/tap-comms.ts
21404
+ init_tap_identity();
21212
21405
  init_tap_utils();
21213
21406
  init_tap_io();
21214
21407
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
@@ -21416,6 +21609,7 @@ init_tap_utils();
21416
21609
  import { existsSync as existsSync4, readFileSync as readFileSync4, statSync as statSync4, watch } from "fs";
21417
21610
  import { join as join4 } from "path";
21418
21611
  init_tap_io();
21612
+ init_tap_identity();
21419
21613
  var notifiedFiles = /* @__PURE__ */ new Set();
21420
21614
  var recentEvents = /* @__PURE__ */ new Map();
21421
21615
  var inFlightFiles = /* @__PURE__ */ new Set();
@@ -21454,7 +21648,7 @@ async function waitForFileReady(filepath) {
21454
21648
  function isOwnMessageArtifact(source, filename, parsed) {
21455
21649
  const agentId = getAgentId();
21456
21650
  const agentName = getAgentName();
21457
- if (parsed && (parsed.from === agentId || parsed.from === agentName)) {
21651
+ if (parsed && isOwnMessageAddress(parsed.from, agentId, agentName)) {
21458
21652
  return true;
21459
21653
  }
21460
21654
  if (source === "reviews") {
@@ -21476,9 +21670,6 @@ async function processWatchFile(dir, source, filename, mcp2) {
21476
21670
  const key = getSourceKey(source, filename);
21477
21671
  if (notifiedFiles.has(key) || inFlightFiles.has(key) || readFiles.has(key))
21478
21672
  return false;
21479
- const parsed = parseFilename(filename);
21480
- if (source === "inbox" && (!parsed || !isForMe(parsed.to))) return false;
21481
- if (isOwnMessageArtifact(source, filename, parsed)) return false;
21482
21673
  inFlightFiles.add(key);
21483
21674
  try {
21484
21675
  const filepath = join4(dir, filename);
@@ -21488,6 +21679,15 @@ async function processWatchFile(dir, source, filename, mcp2) {
21488
21679
  return false;
21489
21680
  }
21490
21681
  if (!file2) return false;
21682
+ let parsed = null;
21683
+ if (source === "inbox") {
21684
+ const fm = parseFrontmatter(file2.content);
21685
+ parsed = fm ? { from: fm.from, to: fm.to, subject: fm.subject } : parseFilename(filename);
21686
+ } else {
21687
+ parsed = parseFilename(filename);
21688
+ }
21689
+ if (source === "inbox" && (!parsed || !isForMe(parsed.to))) return false;
21690
+ if (isOwnMessageArtifact(source, filename, parsed)) return false;
21491
21691
  const rawFrom = parsed?.from || source;
21492
21692
  const rawTo = parsed?.to || "all";
21493
21693
  const from = parsed ? resolveAgentLabel(parsed.from) : source;
@@ -21579,7 +21779,7 @@ import { readdirSync as readdirSync5, renameSync as renameSync2, statSync as sta
21579
21779
  // ../tap-plugin/channels/tap-poll-fallback.ts
21580
21780
  init_tap_utils();
21581
21781
  import { existsSync as existsSync5, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
21582
- var POLL_INTERVAL_MS = 3e4;
21782
+ var POLL_INTERVAL_MS = process.platform === "win32" ? 1e4 : 3e4;
21583
21783
  var POLL_SOURCES = ["inbox", "reviews"];
21584
21784
  var recoveredCount = 0;
21585
21785
  var pollCycles = 0;
@@ -21657,8 +21857,8 @@ function loadOnboardingTeaser() {
21657
21857
  const commsDir = process.env.TAP_COMMS_DIR;
21658
21858
  if (!commsDir) return "";
21659
21859
  const stateDir = process.env.TAP_STATE_DIR;
21660
- const agentId = process.env.TAP_AGENT_ID;
21661
- if (stateDir && agentId) {
21860
+ const agentId = getAgentId();
21861
+ if (stateDir && agentId !== "unknown") {
21662
21862
  try {
21663
21863
  const markerPath = join5(stateDir, "onboarded.json");
21664
21864
  if (existsSync6(markerPath)) {
@@ -21673,7 +21873,7 @@ function loadOnboardingTeaser() {
21673
21873
  if (!existsSync6(welcomePath)) return "";
21674
21874
  const content = readFileSync5(welcomePath, "utf-8");
21675
21875
  const lines = content.split("\n").slice(0, ONBOARDING_TEASER_LINES);
21676
- if (stateDir && agentId) {
21876
+ if (stateDir && agentId !== "unknown") {
21677
21877
  try {
21678
21878
  const markerPath = join5(stateDir, "onboarded.json");
21679
21879
  let store = {};
@@ -21883,11 +22083,22 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
21883
22083
  }
21884
22084
  ]
21885
22085
  }));
22086
+ function prunePhantomHeartbeats(store) {
22087
+ let removed = 0;
22088
+ for (const key of Object.keys(store)) {
22089
+ if (!store[key].id) {
22090
+ delete store[key];
22091
+ removed++;
22092
+ }
22093
+ }
22094
+ return removed;
22095
+ }
21886
22096
  function persistActivity(id, name) {
21887
22097
  const locked = acquireLock(HEARTBEATS_LOCK);
21888
22098
  if (!locked) return;
21889
22099
  try {
21890
22100
  const store = loadHeartbeats();
22101
+ prunePhantomHeartbeats(store);
21891
22102
  const existing = store[id];
21892
22103
  store[id] = {
21893
22104
  id,
@@ -22119,38 +22330,14 @@ Recent active names: ${activeList}`;
22119
22330
  ]
22120
22331
  };
22121
22332
  }
22122
- let cc;
22123
- if (rawCc == null) {
22124
- cc = void 0;
22125
- } else if (typeof rawCc === "string") {
22126
- const trimmed = rawCc.trim();
22127
- cc = trimmed ? [trimmed] : void 0;
22128
- } else if (Array.isArray(rawCc)) {
22129
- const valid = rawCc.filter(
22130
- (v) => typeof v === "string" && v.trim().length > 0
22131
- ).map((v) => v.trim());
22132
- cc = valid.length > 0 ? valid : void 0;
22133
- } else {
22134
- cc = void 0;
22135
- }
22136
- if (cc) {
22137
- const seen = /* @__PURE__ */ new Set([to]);
22138
- cc = cc.filter((r) => {
22139
- if (seen.has(r)) return false;
22140
- seen.add(r);
22141
- return true;
22142
- });
22143
- if (cc.length === 0) cc = void 0;
22144
- }
22145
- const broadcastNames = /* @__PURE__ */ new Set(["\uC804\uCCB4", "all"]);
22333
+ const cc = normalizeRecipientList(rawCc, [to]);
22146
22334
  const recipientWarnings = [];
22147
22335
  const store = loadHeartbeats();
22148
22336
  const knownAgents = /* @__PURE__ */ new Set();
22149
22337
  const displayNameCount = /* @__PURE__ */ new Map();
22150
- const placeholders = /* @__PURE__ */ new Set(["unknown", "unnamed", "<set-per-session>"]);
22151
22338
  for (const [key, hb] of Object.entries(store)) {
22152
- if (!placeholders.has(key)) knownAgents.add(key);
22153
- if (hb.agent && !placeholders.has(hb.agent)) {
22339
+ if (!isPlaceholderAgentValue(key)) knownAgents.add(key);
22340
+ if (!isPlaceholderAgentValue(hb.agent)) {
22154
22341
  knownAgents.add(hb.agent);
22155
22342
  const ids = displayNameCount.get(hb.agent) ?? [];
22156
22343
  ids.push(key);
@@ -22159,7 +22346,7 @@ Recent active names: ${activeList}`;
22159
22346
  }
22160
22347
  const knownList = [...knownAgents].filter((n) => n !== "unknown").join(", ");
22161
22348
  let resolvedTo = to;
22162
- if (!broadcastNames.has(to)) {
22349
+ if (!isBroadcastRecipient(to)) {
22163
22350
  if (!knownAgents.has(to)) {
22164
22351
  recipientWarnings.push(
22165
22352
  `\u26A0\uFE0F WARNING: "${to}" is not a known agent. Check spelling. Known: ${knownList}`
@@ -22176,7 +22363,7 @@ Recent active names: ${activeList}`;
22176
22363
  }
22177
22364
  if (cc?.length) {
22178
22365
  for (const recipient of cc) {
22179
- if (broadcastNames.has(recipient)) continue;
22366
+ if (isBroadcastRecipient(recipient)) continue;
22180
22367
  if (!knownAgents.has(recipient)) {
22181
22368
  recipientWarnings.push(
22182
22369
  `\u26A0\uFE0F WARNING: CC "${recipient}" is not a known agent. Known: ${knownList}`
@@ -22191,7 +22378,8 @@ Recent active names: ${activeList}`;
22191
22378
  }
22192
22379
  }
22193
22380
  }
22194
- const date4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
22381
+ const now = /* @__PURE__ */ new Date();
22382
+ const date4 = now.toISOString().slice(0, 10).replace(/-/g, "");
22195
22383
  const fromId = getAgentId();
22196
22384
  const fromName = getAgentName();
22197
22385
  const filename = `${date4}-${fromId}-${resolvedTo}-${subject}.md`;
@@ -22199,7 +22387,19 @@ Recent active names: ${activeList}`;
22199
22387
  const ccHeader = cc?.length ? `> CC: ${cc.join(", ")}
22200
22388
 
22201
22389
  ` : "";
22202
- writeFileSync2(filepath, ccHeader + content, "utf-8");
22390
+ const frontmatter = [
22391
+ "---",
22392
+ "type: inbox",
22393
+ `from: ${fromId}`,
22394
+ `from_name: ${fromName}`,
22395
+ `to: ${resolvedTo}`,
22396
+ `to_name: ${to}`,
22397
+ `subject: ${subject}`,
22398
+ `sent_at: ${now.toISOString()}`,
22399
+ "---",
22400
+ ""
22401
+ ].join("\n");
22402
+ writeFileSync2(filepath, frontmatter + ccHeader + content, "utf-8");
22203
22403
  dbInsertMessage(
22204
22404
  filename,
22205
22405
  fromName,
@@ -22213,16 +22413,28 @@ Recent active names: ${activeList}`;
22213
22413
  const writtenFiles = /* @__PURE__ */ new Set([filename]);
22214
22414
  for (const recipient of cc) {
22215
22415
  try {
22216
- const resolvedRecipient = broadcastNames.has(recipient) ? recipient : resolveToMostRecent2(recipient);
22416
+ const resolvedRecipient = isBroadcastRecipient(recipient) ? recipient : resolveToMostRecent2(recipient);
22217
22417
  const ccFilename = `${date4}-${fromId}-${resolvedRecipient}-${subject}.md`;
22218
22418
  if (writtenFiles.has(ccFilename)) {
22219
22419
  sent.push(`CC to ${recipient}: skipped (resolves to same target)`);
22220
22420
  continue;
22221
22421
  }
22222
22422
  writtenFiles.add(ccFilename);
22423
+ const ccFrontmatter = [
22424
+ "---",
22425
+ "type: inbox",
22426
+ `from: ${fromId}`,
22427
+ `from_name: ${fromName}`,
22428
+ `to: ${resolvedRecipient}`,
22429
+ `to_name: ${recipient}`,
22430
+ `subject: ${subject}`,
22431
+ `sent_at: ${now.toISOString()}`,
22432
+ "---",
22433
+ ""
22434
+ ].join("\n");
22223
22435
  writeFileSync2(
22224
22436
  join5(INBOX_DIR, ccFilename),
22225
- `> CC from message to ${to}
22437
+ ccFrontmatter + `> CC from message to ${to}
22226
22438
 
22227
22439
  ${content}`,
22228
22440
  "utf-8"
@@ -22248,11 +22460,27 @@ ${content}`,
22248
22460
  }
22249
22461
  if (req.params.name === "tap_broadcast") {
22250
22462
  const { subject, content } = req.params.arguments;
22251
- const date4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
22463
+ const now = /* @__PURE__ */ new Date();
22464
+ const date4 = now.toISOString().slice(0, 10).replace(/-/g, "");
22252
22465
  const broadcastId = getAgentId();
22253
22466
  const broadcastName = getAgentName();
22254
22467
  const filename = `${date4}-${broadcastId}-\uC804\uCCB4-${subject}.md`;
22255
- writeFileSync2(join5(INBOX_DIR, filename), content, "utf-8");
22468
+ const broadcastFrontmatter = [
22469
+ "---",
22470
+ "type: inbox",
22471
+ `from: ${broadcastId}`,
22472
+ `from_name: ${broadcastName}`,
22473
+ "to: \uC804\uCCB4",
22474
+ `subject: ${subject}`,
22475
+ `sent_at: ${now.toISOString()}`,
22476
+ "---",
22477
+ ""
22478
+ ].join("\n");
22479
+ writeFileSync2(
22480
+ join5(INBOX_DIR, filename),
22481
+ broadcastFrontmatter + content,
22482
+ "utf-8"
22483
+ );
22256
22484
  dbInsertMessage(
22257
22485
  filename,
22258
22486
  broadcastName,
@@ -22309,9 +22537,30 @@ ${content}`,
22309
22537
  releaseLock(RECEIPTS_LOCK);
22310
22538
  }
22311
22539
  }
22540
+ function buildHudLine() {
22541
+ const hbStore = loadHeartbeats();
22542
+ const aliveWindow = Date.now() - 10 * 60 * 1e3;
22543
+ let agentCount = 0;
22544
+ for (const hb of Object.values(hbStore)) {
22545
+ if (!hb.id) continue;
22546
+ const t = new Date(hb.lastActivity ?? hb.timestamp ?? 0).getTime();
22547
+ if (t >= aliveWindow && hb.status !== "signing-off") agentCount++;
22548
+ }
22549
+ const unreadItems = getUnreadItems({
22550
+ sources: ["inbox"],
22551
+ limit: 100,
22552
+ includeContent: false,
22553
+ markRead: false
22554
+ });
22555
+ const unreadCount = unreadItems.length;
22556
+ const unreadDisplay = unreadCount >= 100 ? "99+" : String(unreadCount);
22557
+ const status = agentCount > 0 ? "\u{1F7E2}" : "\u26AA";
22558
+ return `[tap] ${status} ${agentCount} agents | \u{1F4E8} ${unreadDisplay} unread`;
22559
+ }
22312
22560
  if (req.params.name === "tap_stats") {
22313
22561
  const hours = typeof req.params.arguments?.hours === "number" ? req.params.arguments.hours : 24;
22314
22562
  const cutoff = Date.now() - hours * 60 * 60 * 1e3;
22563
+ const hud = buildHudLine();
22315
22564
  const dbResult = dbGetStats(cutoff);
22316
22565
  if (dbResult) {
22317
22566
  return {
@@ -22319,7 +22568,7 @@ ${content}`,
22319
22568
  {
22320
22569
  type: "text",
22321
22570
  text: JSON.stringify(
22322
- { hours, ...dbResult, source: "sqlite" },
22571
+ { hours, ...dbResult, source: "sqlite", hud },
22323
22572
  null,
22324
22573
  2
22325
22574
  )
@@ -22342,7 +22591,7 @@ ${content}`,
22342
22591
  const parsed = parseFilename2(filename);
22343
22592
  if (!parsed) continue;
22344
22593
  sent[parsed.from] = (sent[parsed.from] || 0) + 1;
22345
- if (parsed.to === "\uC804\uCCB4" || parsed.to === "all") broadcasts++;
22594
+ if (isBroadcastRecipient(parsed.to)) broadcasts++;
22346
22595
  else received[parsed.to] = (received[parsed.to] || 0) + 1;
22347
22596
  }
22348
22597
  }
@@ -22357,7 +22606,14 @@ ${content}`,
22357
22606
  {
22358
22607
  type: "text",
22359
22608
  text: JSON.stringify(
22360
- { hours, sent, received, broadcasts, totalReceipts: receiptCount },
22609
+ {
22610
+ hours,
22611
+ sent,
22612
+ received,
22613
+ broadcasts,
22614
+ totalReceipts: receiptCount,
22615
+ hud
22616
+ },
22361
22617
  null,
22362
22618
  2
22363
22619
  )
@@ -22405,6 +22661,7 @@ ${content}`,
22405
22661
  const store = loadHeartbeats();
22406
22662
  const agents = [];
22407
22663
  for (const [agentId, hb] of Object.entries(store)) {
22664
+ if (!hb.id) continue;
22408
22665
  const actTime = new Date(hb.lastActivity).getTime();
22409
22666
  if (actTime < cutoff) continue;
22410
22667
  const alive = hb.status !== "signing-off";