granttap-mcp 0.8.85 → 0.8.87

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.
Files changed (42) hide show
  1. package/README.md +4 -0
  2. package/apps/bridge/src/host/session-keys.ts +5 -1
  3. package/apps/bridge/src/mesh/catalog/README.md +10 -0
  4. package/apps/bridge/src/mesh/catalog/index.ts +1 -0
  5. package/apps/bridge/src/mesh/catalog/repository-inspection.ts +49 -0
  6. package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +8 -8
  7. package/apps/bridge/src/monitor/capability-publication/README.md +13 -0
  8. package/apps/bridge/src/monitor/capability-publication/index.ts +38 -0
  9. package/apps/bridge/src/monitor/support/runtime.ts +31 -29
  10. package/apps/bridge/src/session-catalog/README.md +5 -4
  11. package/apps/bridge/src/session-catalog/publish-heads.ts +10 -4
  12. package/apps/bridge/src/sessions/README.md +8 -0
  13. package/apps/bridge/src/sessions/index.ts +26 -5
  14. package/apps/bridge/src/sessions/native-capability-history/README.md +20 -0
  15. package/apps/bridge/src/sessions/native-capability-history/completed-item.ts +31 -0
  16. package/apps/bridge/src/sessions/native-capability-history/index.ts +67 -0
  17. package/apps/bridge/src/sessions/native-capability-history/reader.js +97 -0
  18. package/apps/bridge/src/sessions/scan/claude/activity.ts +10 -3
  19. package/apps/bridge/src/sessions/scan/claude/index.ts +1 -0
  20. package/apps/bridge/src/sessions/scan/claude/parse.ts +6 -2
  21. package/apps/bridge/src/sessions/scan/claude/recorded-usage/README.md +17 -0
  22. package/apps/bridge/src/sessions/scan/claude/recorded-usage/index.ts +60 -0
  23. package/apps/bridge/src/sessions/scan/codex/activity.ts +15 -5
  24. package/apps/bridge/src/sessions/scan/grok.ts +12 -2
  25. package/apps/bridge/src/sessions/support/capability-totals.ts +0 -0
  26. package/apps/bridge/src/sessions/telemetry/execution-summaries.ts +56 -0
  27. package/apps/mcp/src/desktop/task-activity-worker.ts +4 -4
  28. package/apps/mcp/src/http/README.md +15 -0
  29. package/apps/mcp/src/http/http-server.ts +1 -3
  30. package/cursor-plugin/README.md +1 -1
  31. package/cursor-plugin/mcp.json +1 -1
  32. package/cursor-plugin/stdio-bootstrap.cjs +1 -1
  33. package/package.json +1 -1
  34. package/packages/core/relay-client-types.ts +6 -0
  35. package/packages/core/relay-client.ts +14 -19
  36. package/packages/core/relay-envelope.ts +7 -3
  37. package/packages/core/relay-inbound/index.ts +2 -0
  38. package/packages/core/relay-inbound/wait-for.ts +19 -0
  39. package/packages/core/retained-state/README.md +23 -0
  40. package/packages/core/retained-state/index.ts +25 -0
  41. package/packages/protocol/messages/capabilities.ts +17 -0
  42. package/packages/protocol/schema.ts +4 -0
package/README.md CHANGED
@@ -843,3 +843,7 @@ sustained structured work in a different checkout without interpreting chat
843
843
  titles. Task identity and its original coordination/access scope survive.
844
844
 
845
845
  See [repository observations](apps/bridge/src/mesh/repository-details/README.md).
846
+
847
+ The relay can retain recipient-encrypted catalogs, Mesh snapshots, capability usage and recent chat heads for reconnect without the computer. This bounded seven-day read cache is separate from the durable encrypted command queue. See [relay state publication](packages/core/retained-state/README.md). The relay never receives decryption keys or readable chat content.
848
+
849
+ Tools & Skills matches recorded usage to the exact computer, provider and execution. Codex's native history projection supplies actual MCP invocation IDs, results and durations, including calls made through Code Mode. Claude Code uses observed tool-use, result and Skill records, aggregating MCP and skill calls before its recent-detail cache is trimmed. Execution summaries remain available when older call details fall outside the recent-event budget. Counts describe recorded evidence; installation and permission do not prove usage. Missing resource measurements remain unknown. See [native capability history](apps/bridge/src/sessions/native-capability-history/README.md).
@@ -1,3 +1,4 @@
1
+ import { retainedPayloadOptions } from "../../../../packages/core/retained-state";
1
2
  import { existsSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { loadSessionKeys, persistedSessionKey } from "./session-key-store";
@@ -70,6 +71,9 @@ async function sendScopedPayload(input: {
70
71
  options: SendOptions;
71
72
  }): Promise<void> {
72
73
  const { client, payload, sessionId, purpose, to, options } = input;
74
+ const retained = retainedPayloadOptions(payload);
75
+ const grantOptions = retained.stateKey ? {}
76
+ : { stateKey: undefined, stateRevision: undefined, statePriority: undefined, stateGroup: undefined };
73
77
  const key = sessionKey(sessionId);
74
78
  client.setSessionKey(sessionId, key);
75
79
  await client.send({
@@ -78,6 +82,6 @@ async function sendScopedPayload(input: {
78
82
  key,
79
83
  purpose,
80
84
  createdAt: Date.now(),
81
- }, to, { ttlMs: options.ttlMs, peerPublicKey: options.peerPublicKey });
85
+ }, to, { ttlMs: options.ttlMs, peerPublicKey: options.peerPublicKey, ...grantOptions });
82
86
  await client.sendSession(payload, sessionId, to, options);
83
87
  }
@@ -17,6 +17,16 @@ negative probes nor old repository identities persist across refreshes.
17
17
  Behavior tests: `tests/agentic/codex-workspace-binding.test.ts` and
18
18
  `tests/project/project-mesh-core.test.ts`.
19
19
 
20
+ ## Background Graph admission
21
+
22
+ `inspectRepositoryAsync` provides the same repository facts without blocking
23
+ relay callbacks. Native Git subprocesses are limited to two across the process.
24
+ Concurrent inspections of the same path share their in-flight result; a later
25
+ inspection verifies the checkout again. Missing Git facts remain unknown.
26
+
27
+ Tests for asynchronous inspection live in `tests/`; Graph admission also has
28
+ `runtime/graph/tests/responsiveness/checkout-inspection.test.ts`.
29
+
20
30
  ## License
21
31
 
22
32
  GrantTap Commercial Source License. See [LICENSE](../../../../../LICENSE).
@@ -19,6 +19,7 @@ import { readIntegrationMap } from "../observed/integration-map";
19
19
  import type { MeshStore } from "../store";
20
20
  import { queueProjectBindingSync } from "../../engine/runtime/engine-projects";
21
21
  import { isCursorTaskCloneId } from "../../sessions/cursor/catalog";
22
+ export { inspectRepositoryAsync } from "./repository-inspection";
22
23
 
23
24
  export type RepositoryFacts = {
24
25
  root: string;
@@ -0,0 +1,49 @@
1
+ import { execFile } from "node:child_process";
2
+ import { canonicalRepositoryIdentity, sanitizedRepositoryRemote } from "../identity";
3
+ import type { RepositoryFacts } from ".";
4
+
5
+ let running = 0;
6
+ const waiting: Array<() => void> = [];
7
+ const pending = new Map<string, Promise<RepositoryFacts>>();
8
+
9
+ /** Native read-only Git work stays bounded without blocking relay callbacks. */
10
+ function git(cwd: string, args: string[]): Promise<string | undefined> {
11
+ return new Promise(resolve => {
12
+ const run = () => {
13
+ running++;
14
+ const done = (error: Error | null, stdout = "") => {
15
+ running--;
16
+ waiting.shift()?.();
17
+ resolve(error ? undefined : stdout.trim() || undefined);
18
+ };
19
+ try {
20
+ execFile("git", ["-C", cwd, ...args], {
21
+ encoding: "utf8", timeout: 2_000, maxBuffer: 1024 * 1024, windowsHide: true,
22
+ }, done);
23
+ } catch { done(new Error("Repository command could not start")); }
24
+ };
25
+ if (running < 2) run(); else waiting.push(run);
26
+ });
27
+ }
28
+
29
+ async function inspect(cwd: string): Promise<RepositoryFacts> {
30
+ const worktree = await git(cwd, ["rev-parse", "--show-toplevel"]);
31
+ const root = worktree ?? cwd;
32
+ const [rawRemote, revision, branch] = await Promise.all([
33
+ git(root, ["remote", "get-url", "origin"]),
34
+ git(root, ["rev-parse", "HEAD"]),
35
+ git(root, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
36
+ ]);
37
+ return { root, worktree, revision, branch,
38
+ baseRemote: rawRemote ? sanitizedRepositoryRemote(rawRemote) : undefined,
39
+ canonicalRepositoryId: canonicalRepositoryIdentity(rawRemote, root) };
40
+ }
41
+
42
+ /** Share only in-flight observations; a later refresh verifies the checkout again. */
43
+ export function inspectRepositoryAsync(cwd: string): Promise<RepositoryFacts> {
44
+ const active = pending.get(cwd);
45
+ if (active) return active;
46
+ const next = inspect(cwd).finally(() => { pending.delete(cwd); });
47
+ pending.set(cwd, next);
48
+ return next;
49
+ }
@@ -1,7 +1,7 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import type { MeshSnapshot, ProjectRepositoryGraph } from "../../../../../../packages/protocol/schema";
3
3
  import { queueProjectBindingSync } from "../../../engine/runtime/engine-projects";
4
- import { inspectRepository } from "../../catalog";
4
+ import { inspectRepositoryAsync, type RepositoryFacts } from "../../catalog";
5
5
  import { computerId } from "../../identity/computer";
6
6
  import { matchesLocalCheckout } from "./checkout-identity";
7
7
 
@@ -18,18 +18,18 @@ export async function refreshLocalGraphBindings(
18
18
  snapshot: MeshSnapshot,
19
19
  dependencies: {
20
20
  endpoint: () => string;
21
- inspect: typeof inspectRepository;
21
+ inspect: (cwd: string) => RepositoryFacts | Promise<RepositoryFacts>;
22
22
  sync: typeof queueProjectBindingSync;
23
- } = { endpoint: computerId, inspect: inspectRepository, sync: queueProjectBindingSync },
23
+ } = { endpoint: computerId, inspect: inspectRepositoryAsync, sync: queueProjectBindingSync },
24
24
  ): Promise<ProjectRepositoryGraph[]> {
25
25
  const localEndpoint = dependencies.endpoint();
26
26
  const candidates = (snapshot.bindings ?? []).filter((binding) =>
27
27
  binding.endpointId === localEndpoint && binding.available && binding.localPathHint);
28
28
  const failures: ProjectRepositoryGraph[] = [];
29
- const checkouts = candidates.flatMap((binding) => {
30
- const facts = readCheckout(dependencies.inspect, binding.localPathHint!);
29
+ const checkouts = (await Promise.all(candidates.map(async binding => {
30
+ const facts = await readCheckout(dependencies.inspect, binding.localPathHint!);
31
31
  return facts ? [{ binding, facts, real: realpathSync(facts.root) }] : [];
32
- });
32
+ }))).flat();
33
33
  await Promise.all(checkouts.map(async ({ binding, facts, real }) => {
34
34
  const pathIdentity = matchesLocalCheckout(binding.repositoryId, facts.root);
35
35
  if (facts.canonicalRepositoryId !== binding.repositoryId && !pathIdentity) {
@@ -58,9 +58,9 @@ export async function refreshLocalGraphBindings(
58
58
  }
59
59
 
60
60
  /** A Git checkout rooted exactly at the bound path, or nothing a Graph can be built from. */
61
- function readCheckout(inspect: typeof inspectRepository, path: string) {
61
+ async function readCheckout(inspect: (cwd: string) => RepositoryFacts | Promise<RepositoryFacts>, path: string) {
62
62
  try {
63
- const facts = inspect(path);
63
+ const facts = await inspect(path);
64
64
  if (!facts.worktree || realpathSync(facts.root) !== realpathSync(path)) return undefined;
65
65
  return facts;
66
66
  } catch {
@@ -0,0 +1,13 @@
1
+ # Usage publication
2
+
3
+ Public entry: `index.ts`. Complete capability totals are computed from the live
4
+ catalog and full discovered history. This work has one independent in-flight
5
+ publication per relay client. A slow history read or encrypted fan-out does not
6
+ hold the current chat catalog. Successful reports retain their existing cadence;
7
+ failures retry on a later catalog publication. Closing the monitor prevents
8
+ further queued reads and sends. Counts and unknown usage semantics are unchanged.
9
+ Concurrent catalog and Usage requests share one in-flight history read. The
10
+ history cache's lifetime begins when that read completes.
11
+
12
+ Behavior tests live in `tests/`.
13
+ Licensed under the GrantTap Commercial Source License in the repository root.
@@ -0,0 +1,38 @@
1
+ import { setImmediate } from "node:timers/promises";
2
+ import type { RelayClient } from "../../../../../packages/core/relay-client";
3
+ import type { SessionInfo, SessionsStatus } from "../../../../../packages/protocol/schema";
4
+ import { scanRecordedCapabilityUsage, scopeCapabilityUsageToRoom } from "../../sessions";
5
+
6
+ type Publication = { running: boolean; closed: boolean; lastPublishedAt: number };
7
+ const publications = new WeakMap<RelayClient, Publication>();
8
+
9
+ /** A slow usage fan-out never occupies the current chat catalog's publication slot. */
10
+ export function publishMonitorCapabilities(client: RelayClient, status: SessionsStatus,
11
+ history: () => Promise<SessionInfo[]>): void {
12
+ const state = publications.get(client) ?? { running: false, closed: false, lastPublishedAt: 0 };
13
+ publications.set(client, state);
14
+ if (state.running || state.closed || Date.now() - state.lastPublishedAt < 30_000) return;
15
+ state.running = true;
16
+ const work = (async () => {
17
+ await setImmediate();
18
+ if (state.closed || !client.isConnected) return;
19
+ const publish = async (sessions: SessionInfo[]) => {
20
+ const usage = await scanRecordedCapabilityUsage(sessions);
21
+ if (state.closed || !client.isConnected) return;
22
+ await client.send(scopeCapabilityUsageToRoom(usage, client.room), "phone",
23
+ { ttlMs: 90_000, reliable: false });
24
+ };
25
+ await publish(status.sessions);
26
+ const older = await history();
27
+ if (state.closed || !client.isConnected) return;
28
+ if (older.length) await publish([...status.sessions, ...older]);
29
+ state.lastPublishedAt = Date.now();
30
+ })();
31
+ void work.catch(() => {}).finally(() => { state.running = false; });
32
+ }
33
+
34
+ export function stopMonitorCapabilities(client: RelayClient): void {
35
+ const state = publications.get(client);
36
+ if (state) state.closed = true;
37
+ publications.delete(client);
38
+ }
@@ -23,7 +23,7 @@ import { publishedCatalog, rememberPublishedCatalog, resumePublishedChats,
23
23
  publishCatalogHeads, stopCatalogHeads } from "../../session-catalog";
24
24
  import { createMachineLoadPublisher } from "../../machine-load";
25
25
  import { startMachineLoadLoop } from "../../machine-load/host/loop";
26
- import { scanCapabilityUsage, scanSessionHistoryCooperatively, scanSessionsCooperatively, scopeCapabilityUsageToRoom, TOKEN_WINDOW_HOURS } from "../../sessions";
26
+ import { scanSessionHistoryCooperatively, scanSessionsCooperatively, TOKEN_WINDOW_HOURS } from "../../sessions";
27
27
  import { boundedCatalogHistory, INTERVAL_MS, publishSessionEvents } from "../handlers/catalog";
28
28
  import { sweepAttachments } from "../handlers/receipts";
29
29
  import { handleMonitorMessage } from "../handlers/inbound";
@@ -32,6 +32,7 @@ import { decorateTaskOriginCatalog } from '../../task-origins';
32
32
  const HISTORY_INTERVAL_MS = 60_000;
33
33
  const resumedHeads = new WeakSet<RelayClient>();
34
34
  import { publishMonitorMeshes, stopMonitorMeshes } from "../mesh-publication";
35
+ import { publishMonitorCapabilities, stopMonitorCapabilities } from "../capability-publication";
35
36
 
36
37
  export type SessionMonitor = {
37
38
  publish: () => Promise<void>;
@@ -114,30 +115,33 @@ async function publishMonitorTick(input: {
114
115
  leadership: ReturnType<typeof monitorLeadership>;
115
116
  subscriptions: Set<string>;
116
117
  history: (force?: boolean) => Promise<SessionsStatus["sessions"]>;
117
- clocks: { lastHistoryPublishedAt: number; lastCapabilityPublishedAt: number };
118
+ clocks: { lastHistoryPublishedAt: number };
118
119
  loadLoop: { updateStatus: (status: SessionsStatus) => void };
119
120
  forceHistory: boolean;
120
121
  active: () => boolean;
121
- }): Promise<{ lastHistoryPublishedAt: number; lastCapabilityPublishedAt: number }> {
122
+ }): Promise<{ lastHistoryPublishedAt: number }> {
122
123
  const { client, leadership, subscriptions, history, clocks, loadLoop, forceHistory } = input;
123
- let { lastHistoryPublishedAt, lastCapabilityPublishedAt } = clocks;
124
- if (!client.isConnected) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
124
+ let { lastHistoryPublishedAt } = clocks;
125
+ if (!client.isConnected) return { lastHistoryPublishedAt };
125
126
  await publishHeartbeat(client).catch(() => {});
126
- if (!input.active()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
127
- if (!leadership.acquire()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
127
+ if (!input.active()) return { lastHistoryPublishedAt };
128
+ if (!leadership.acquire()) return { lastHistoryPublishedAt };
128
129
  await client.send(approvalsStatus(), "phone", { ttlMs: INTERVAL_MS * 3, reliable: false });
129
130
  const initial = lastHistoryPublishedAt === 0;
130
- const includeHistory = forceHistory || (!initial && Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS);
131
- const snapshot = await monitorSnapshot({ includeHistory, forceHistory, subscriptions, history });
132
- if (!input.active()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
133
- const status = decorateTaskOriginCatalog(snapshot, client.room);
134
- rememberPublishedCatalog(client, status);
135
- await client.send(status, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
136
131
  if (initial && !resumedHeads.has(client)) {
132
+ const saved = publishedCatalog(client);
133
+ if (saved) await client.send(saved, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
134
+ if (!input.active()) return { lastHistoryPublishedAt };
137
135
  resumedHeads.add(client);
138
136
  void resumePublishedChats(client,
139
137
  (session, saved) => publishSessionEvents(client, session.sessionId, saved)).catch(() => {});
140
138
  }
139
+ const includeHistory = forceHistory || (!initial && Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS);
140
+ const snapshot = await monitorSnapshot({ includeHistory, forceHistory, subscriptions, history });
141
+ if (!input.active()) return { lastHistoryPublishedAt };
142
+ const status = decorateTaskOriginCatalog(snapshot, client.room);
143
+ rememberPublishedCatalog(client, status);
144
+ await client.send(status, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
141
145
  // Provider review is independent of task visibility; a hung hook RPC must
142
146
  // never hold the current catalog behind its discovery timeout.
143
147
  void refreshCodexHooks().catch(() => {});
@@ -146,15 +150,8 @@ async function publishMonitorTick(input: {
146
150
  void refreshMcpLoad(status.sessions).catch(() => {});
147
151
  publishCatalogHeads(client, status, subscriptions, publishSessionEvents);
148
152
  if (loadRuntimeConfig().meshEnabled) publishMonitorMeshes(client, status);
149
- if (Date.now() - lastCapabilityPublishedAt >= 30_000) {
150
- const usage = scanCapabilityUsage([
151
- ...status.sessions,
152
- ...await history(false),
153
- ]);
154
- await client.send(scopeCapabilityUsageToRoom(usage, client.room), "phone", { ttlMs: 90_000, reliable: false });
155
- lastCapabilityPublishedAt = Date.now();
156
- }
157
- return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
153
+ publishMonitorCapabilities(client, status, () => history(false));
154
+ return { lastHistoryPublishedAt };
158
155
  }
159
156
 
160
157
  /**
@@ -168,8 +165,8 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
168
165
  primeSessionKeys(client);
169
166
  publishedCatalog(client);
170
167
  let historyCache: { generatedAt: number; sessions: SessionsStatus["sessions"] } | undefined;
168
+ let historyRead: Promise<SessionsStatus["sessions"]> | undefined;
171
169
  let lastHistoryPublishedAt = 0;
172
- let lastCapabilityPublishedAt = 0;
173
170
  let closed = false;
174
171
  const publishMachineLoad = createMachineLoadPublisher({
175
172
  log: (line) => process.stderr.write(`[monitor] ${line}\n`),
@@ -189,11 +186,16 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
189
186
  },
190
187
  });
191
188
 
192
- const history = async (force = false): Promise<SessionsStatus["sessions"]> => {
193
- if (force || !historyCache || Date.now() - historyCache.generatedAt > 60_000) {
194
- historyCache = { generatedAt: Date.now(), sessions: await scanSessionHistoryCooperatively() };
189
+ const history = (force = false): Promise<SessionsStatus["sessions"]> => {
190
+ if (historyRead) return historyRead;
191
+ if (!force && historyCache && Date.now() - historyCache.generatedAt <= 60_000) {
192
+ return Promise.resolve(historyCache.sessions);
195
193
  }
196
- return historyCache.sessions;
194
+ historyRead = scanSessionHistoryCooperatively().then(sessions => {
195
+ historyCache = { generatedAt: Date.now(), sessions };
196
+ return sessions;
197
+ }).finally(() => { historyRead = undefined; });
198
+ return historyRead;
197
199
  };
198
200
 
199
201
  const publish = singleFlightPublisher(async (forceHistory: boolean): Promise<void> => {
@@ -201,10 +203,9 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
201
203
  const next = await publishMonitorTick({
202
204
  client, leadership, subscriptions, history, loadLoop, forceHistory,
203
205
  active: () => !closed,
204
- clocks: { lastHistoryPublishedAt, lastCapabilityPublishedAt },
206
+ clocks: { lastHistoryPublishedAt },
205
207
  });
206
208
  lastHistoryPublishedAt = next.lastHistoryPublishedAt;
207
- lastCapabilityPublishedAt = next.lastCapabilityPublishedAt;
208
209
  });
209
210
 
210
211
  const off = client.onMessage(async (payload: Payload, peerPublicKey) => {
@@ -245,6 +246,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
245
246
  offConnected?.();
246
247
  stopCatalog();
247
248
  stopCatalogHeads(client);
249
+ stopMonitorCapabilities(client);
248
250
  stopMonitorMeshes(client);
249
251
  stopHeartbeat();
250
252
  loadLoop.stop();
@@ -9,10 +9,11 @@ unreadable merely because hundreds of newer logs appeared.
9
9
  The private `session-catalog-cache.json` contains metadata and validated native
10
10
  source locations. It excludes messages, capability descriptions, and credentials.
11
11
  The room and provider roots bind its scope. On restart the primary monitor sends
12
- a fresh live catalog first, then resumes the saved Now chats' native heads under
13
- their independent task keys in the background. Closed-history discovery runs
14
- separately from that first live publication. It does not republish an old catalog
15
- as fresh. The phone continues its durable historical backfill independently.
12
+ the saved catalog with its original timestamp before starting fresh discovery,
13
+ and resumes the saved Now chats' native heads under their independent task keys
14
+ in the background. Fresh discovery replaces that saved state. Closed-history
15
+ discovery runs separately from the first live publication. The phone continues
16
+ its durable historical backfill independently.
16
17
 
17
18
  Tests live in `tests/`. This runtime module uses the repository GrantTap Commercial Source License.
18
19
 
@@ -1,19 +1,25 @@
1
1
  import type { RelayClient } from "../../../../packages/core/relay-client";
2
2
  import type { SessionsStatus } from "../../../../packages/protocol/schema";
3
3
 
4
- type HeadRun = { running: boolean; closed: boolean };
4
+ type HeadRun = { running: boolean; closed: boolean; cursor: number };
5
5
  const runs = new WeakMap<RelayClient, HeadRun>();
6
6
 
7
7
  /** Transcript transfers must never occupy the fresh catalog's publication slot. */
8
8
  export function publishCatalogHeads(client: RelayClient, status: SessionsStatus,
9
9
  subscriptions: ReadonlySet<string>,
10
10
  publish: (client: RelayClient, sessionId: string, status: SessionsStatus) => Promise<unknown>): void {
11
- const state = runs.get(client) ?? { running: false, closed: false };
11
+ const state = runs.get(client) ?? { running: false, closed: false, cursor: 0 };
12
12
  runs.set(client, state);
13
- if (state.running || state.closed || subscriptions.size === 0) return;
13
+ if (state.running || state.closed) return;
14
+ const candidates = [...new Set([...subscriptions,
15
+ ...(client.supportsRetainedState ? status.sessions.map(session => session.sessionId) : [])])];
16
+ if (!candidates.length) return;
17
+ const count = Math.min(32, candidates.length);
18
+ const selected = Array.from({ length: count }, (_, offset) => candidates[(state.cursor + offset) % candidates.length]!);
19
+ state.cursor = (state.cursor + count) % candidates.length;
14
20
  state.running = true;
15
21
  void (async () => {
16
- for (const sessionId of [...subscriptions]) {
22
+ for (const sessionId of selected) {
17
23
  await new Promise<void>(resolve => setImmediate(resolve));
18
24
  if (state.closed || !client.isConnected) break;
19
25
  await publish(client, sessionId, status).catch(() => {});
@@ -18,6 +18,14 @@ the child snapshot. Root chat pagination remains in `TranscriptHistory`.
18
18
  Feature tests live in `tests/`; the reusable multi-provider conversation harness
19
19
  lives in the repository `tests/thread/` directory.
20
20
 
21
+ ## Grok Build history
22
+
23
+ Grok discovery reads the local session summaries and visible conversation history.
24
+ Both role-based records and current typed user/assistant records are supported.
25
+ Typed user records must have a valid prompt index. Environment preambles, synthetic
26
+ reminders, system messages, and hidden reasoning are excluded from the timeline.
27
+ Session continuation uses the installed Grok CLI and its existing authentication.
28
+
21
29
  ## License
22
30
 
23
31
  Licensed under the GrantTap Commercial Source License. See LICENSE and NOTICE.
@@ -38,6 +38,9 @@ import {
38
38
  toRemoteCapabilityUsageEvent,
39
39
  } from "./telemetry";
40
40
  import { createCapabilityTotals } from "./support/capability-totals";
41
+ import { createExecutionSummaries, budgetExecutionSummaries } from './telemetry/execution-summaries';
42
+ import { readCodexCapabilityHistory, type NativeCapabilityEvidence } from './native-capability-history';
43
+ import { recordedClaudeUsage } from './scan/claude';
41
44
 
42
45
  export { MAX_ACTIVITY_ENTRIES, MAX_ACTIVITY_TEXT, TOKEN_WINDOW_HOURS };
43
46
  export { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "./native-source";
@@ -115,11 +118,13 @@ export function scanSessionActivity(session: SessionInfo): SessionActivity {
115
118
  */
116
119
  export function scanCapabilityUsage(
117
120
  sessions: SessionInfo[] = [...scanSessions().sessions, ...scanSessionHistory()],
121
+ native?: NativeCapabilityEvidence,
118
122
  ): CapabilityUsageStatus {
119
123
  const candidates: RemoteCapabilityUsageEvent[] = [];
120
124
  // Totals count every observation as it streams past; the candidate list is
121
125
  // trimmed to a byte budget and can never answer for a whole period.
122
126
  const totals = createCapabilityTotals();
127
+ const summaries = createExecutionSummaries();
123
128
  const seenSessions = new Set<string>();
124
129
  for (const session of sessions) {
125
130
  const nativeScope = `${session.agent}\0${session.sessionId}`;
@@ -131,19 +136,35 @@ export function scanCapabilityUsage(
131
136
  : session.agent === "cursor" ? cursorCapabilityUsage(session)
132
137
  : session.agent === "grok" ? grokCapabilityUsage(session)
133
138
  : [];
134
- for (const observation of observations) {
135
- const converted = toRemoteCapabilityUsageEvent(observation);
136
- if (!converted) continue;
139
+ const recorded = session.agent === 'codex' && native?.scopes.includes(session.sessionId);
140
+ const claudeRecorded = session.agent === 'claude' ? recordedClaudeUsage(observations) : undefined;
141
+ const events = observations.map(toRemoteCapabilityUsageEvent).filter(
142
+ (event): event is RemoteCapabilityUsageEvent => event != null
143
+ && !(recorded && event.kind === 'mcp'));
144
+ if (recorded) events.push(...native!.events.filter(event => event.sessionId === session.sessionId));
145
+ for (const converted of events) {
137
146
  const event = { ...converted, agent: session.agent };
138
- totals.add(event);
147
+ if (!claudeRecorded || event.kind === 'cli') {
148
+ totals.add(event);
149
+ summaries.add(event, recorded && event.kind === 'mcp' ? 'native-history' : 'transcript');
150
+ }
139
151
  rememberCapabilityUsageCandidate(candidates, event);
140
152
  }
153
+ for (const summary of claudeRecorded?.summaries ?? []) summaries.merge(summary);
154
+ for (const total of claudeRecorded?.totals ?? []) totals.merge(total);
141
155
  }
142
- const envelope = { type: "capability.usage.status" as const,
156
+ const metadata = { type: "capability.usage.status" as const,
143
157
  totals: totals.rows(), generatedAt: Date.now() };
158
+ const executionSummaries = budgetExecutionSummaries(summaries.rows(), metadata);
159
+ const envelope = { ...metadata, ...(executionSummaries.length ? { executionSummaries } : {}) };
144
160
  return { ...envelope, events: limitCapabilityUsageEvents(candidates, envelope) };
145
161
  }
146
162
 
163
+ /** Native metadata enrichment runs off the relay event loop. */
164
+ export async function scanRecordedCapabilityUsage(sessions: SessionInfo[]): Promise<CapabilityUsageStatus> {
165
+ return scanCapabilityUsage(sessions, await readCodexCapabilityHistory(sessions));
166
+ }
167
+
147
168
  /** Bind every local observation to the authenticated relay room before publish. */
148
169
  export function scopeCapabilityUsageToRoom(
149
170
  status: CapabilityUsageStatus,
@@ -0,0 +1,20 @@
1
+ # Native capability history
2
+
3
+ `index.ts` reads Codex's local history projection through a bounded worker.
4
+ Native MCP execution records provide call identity, outcome, and duration even
5
+ when the call happened through Code Mode and the transcript preview omits it.
6
+ The database is opened read only. Native projection watermarks and the provider's
7
+ updated-page index restrict warm reads to changed calls; a completion replaces
8
+ its original call rather than adding another one. Queries run in a worker with
9
+ a bounded deadline and never occupy the relay event loop. Missing or incompatible projections remain
10
+ unknown; the existing transcript reader remains a compatibility fallback.
11
+
12
+ Only requested executions and their recorded children are queried. The worker
13
+ returns metadata, never prompts, arguments, or tool results. Its thirty-day,
14
+ 20,000-record bound and per-result size bound make summaries observed evidence,
15
+ not a claim of complete lifetime usage. The recent-call list and execution
16
+ summaries are bounded independently. File paths follow the configured Codex
17
+ root and contain no machine-specific fallback.
18
+
19
+ Tests use a real SQLite history projection and reopen it after the writer closes.
20
+ This module uses the repository GrantTap Commercial Source License.
@@ -0,0 +1,31 @@
1
+ import type { SessionInfo } from '../../../../../packages/protocol/schema';
2
+ import type { CapabilityObservation } from '../telemetry';
3
+ import { ts } from '../support/common';
4
+
5
+ /** Only host-authored execution records establish inner Code Mode calls. */
6
+ export function completedCodexMcp(row: any, session: SessionInfo): CapabilityObservation | undefined {
7
+ const payload = row?.payload;
8
+ if (row?.type !== 'event_msg' || payload?.type !== 'item_completed'
9
+ || (payload.thread_id != null && payload.thread_id !== session.sessionId)) return;
10
+ const item = payload.item;
11
+ if (!['McpToolCall', 'mcpToolCall'].includes(item?.type)) return;
12
+ if (![item.id, item.server, item.tool].every(x => typeof x === 'string' && x.trim() && x.length <= 240)) return;
13
+ const start = payload.started_at_ms ?? ts(row.timestamp) ?? session.lastActivityAt;
14
+ const elapsed = item.durationMs ?? (item.duration && Number.isFinite(item.duration.secs)
15
+ && Number.isFinite(item.duration.nanos) ? item.duration.secs * 1000 + item.duration.nanos / 1e6
16
+ : payload.completed_at_ms != null ? payload.completed_at_ms - start : undefined);
17
+ const outcome = item.status === 'failed' || item.result?.isError === true || item.result?.is_error === true
18
+ ? 'error' : ['cancelled', 'canceled'].includes(item.status) ? 'cancelled'
19
+ : item.status === 'completed' ? 'success' : 'unknown';
20
+ return { sourceId: `${session.sessionId}:${item.id}`, sessionId: session.sessionId,
21
+ toolName: `mcp__${item.server}__${item.tool}`, mcpServer: item.server, createdAt: start,
22
+ outcome, ...(Number.isFinite(elapsed) && elapsed >= 0
23
+ ? { durationMs: Math.min(3600000, Math.round(elapsed)) } : {}) };
24
+ }
25
+
26
+ export function recordedCodexToolName(payload: any): string {
27
+ const name = String(payload.name ?? payload.type);
28
+ const namespace = payload.namespace;
29
+ return typeof namespace === 'string' && /^mcp__[A-Za-z0-9_-]+$/.test(namespace)
30
+ ? `${namespace}__${name}` : name;
31
+ }
@@ -0,0 +1,67 @@
1
+ import type { CapabilityExecutionSummary, RemoteCapabilityUsageEvent, SessionInfo } from '../../../../../packages/protocol/schema';
2
+ import { Worker } from 'node:worker_threads';
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { codexSessionsRoot } from '../support/common';
6
+ export { completedCodexMcp, recordedCodexToolName } from './completed-item';
7
+ export type NativeCapabilityEvidence = {
8
+ scopes: string[];
9
+ events: RemoteCapabilityUsageEvent[];
10
+ summaries: CapabilityExecutionSummary[];
11
+ };
12
+
13
+ const empty = (): NativeCapabilityEvidence => ({ scopes: [], events: [], summaries: [] });
14
+ const cache = new Map<string, { at: number; value: Promise<NativeCapabilityEvidence> }>();
15
+ type Cursor = { threadId: string; watermark: number; events: RemoteCapabilityUsageEvent[] };
16
+ const cursors = new Map<string, Cursor[]>();
17
+
18
+ /** Query only bounded call metadata in a worker; native conversation contents stay local. */
19
+ export function readCodexCapabilityHistory(
20
+ sessions: SessionInfo[], root = process.env.GRANTTAP_CODEX_DIR ?? process.env.NODVOX_CODEX_DIR
21
+ ?? process.env.CODEX_HOME ?? dirname(codexSessionsRoot()),
22
+ ): Promise<NativeCapabilityEvidence> {
23
+ const database = join(root, 'thread_history_1.sqlite');
24
+ const owners = new Map<string, string>();
25
+ for (const session of sessions.filter(s => s.agent === 'codex')) {
26
+ owners.set(session.sessionId, session.sessionId);
27
+ for (const child of session.childThreads ?? []) {
28
+ if (!owners.has(child.threadId)) owners.set(child.threadId, session.sessionId);
29
+ }
30
+ }
31
+ const bindings = [...owners].slice(0, 512);
32
+ if (!bindings.length || !existsSync(database)) return Promise.resolve(empty());
33
+ const key = JSON.stringify([database, bindings]);
34
+ const previous = cache.get(key);
35
+ if (previous && Date.now() - previous.at < 30_000) return previous.value;
36
+ const value = readWorker(database, bindings);
37
+ cache.set(key, { at: Date.now(), value });
38
+ while (cache.size > 4) cache.delete(cache.keys().next().value!);
39
+ return value;
40
+ }
41
+
42
+ function readWorker(database: string, bindings: [string, string][]): Promise<NativeCapabilityEvidence> {
43
+ return new Promise(resolve => {
44
+ const requested = new Set(bindings.map(([thread]) => thread));
45
+ const worker = new Worker(new URL('./reader.js', import.meta.url), { execArgv: [],
46
+ workerData: { database, bindings, now: Date.now(),
47
+ previous: (cursors.get(database) ?? []).filter(cursor => requested.has(cursor.threadId)) },
48
+ resourceLimits: { maxOldGenerationSizeMb: 64 } });
49
+ let evidence = empty();
50
+ const timer = setTimeout(() => { void worker.terminate(); }, 45_000);
51
+ worker.once('message', (result: NativeCapabilityEvidence & { cursors?: Cursor[] }) => {
52
+ const { cursors: next, ...publicEvidence } = result;
53
+ if (next) {
54
+ const merged = [...next, ...(cursors.get(database) ?? []).filter(cursor => !requested.has(cursor.threadId))];
55
+ let count = 0;
56
+ cursors.set(database, merged.filter(cursor => {
57
+ count += cursor.events.length;
58
+ return count <= 20000;
59
+ }).slice(0, 512));
60
+ while (cursors.size > 4) cursors.delete(cursors.keys().next().value!);
61
+ }
62
+ evidence = publicEvidence;
63
+ });
64
+ worker.once('error', () => { evidence = empty(); });
65
+ worker.once('exit', code => { clearTimeout(timer); resolve(code === 0 ? evidence : empty()); });
66
+ });
67
+ }