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.
- package/README.md +4 -0
- package/apps/bridge/src/host/session-keys.ts +5 -1
- package/apps/bridge/src/mesh/catalog/README.md +10 -0
- package/apps/bridge/src/mesh/catalog/index.ts +1 -0
- package/apps/bridge/src/mesh/catalog/repository-inspection.ts +49 -0
- package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +8 -8
- package/apps/bridge/src/monitor/capability-publication/README.md +13 -0
- package/apps/bridge/src/monitor/capability-publication/index.ts +38 -0
- package/apps/bridge/src/monitor/support/runtime.ts +31 -29
- package/apps/bridge/src/session-catalog/README.md +5 -4
- package/apps/bridge/src/session-catalog/publish-heads.ts +10 -4
- package/apps/bridge/src/sessions/README.md +8 -0
- package/apps/bridge/src/sessions/index.ts +26 -5
- package/apps/bridge/src/sessions/native-capability-history/README.md +20 -0
- package/apps/bridge/src/sessions/native-capability-history/completed-item.ts +31 -0
- package/apps/bridge/src/sessions/native-capability-history/index.ts +67 -0
- package/apps/bridge/src/sessions/native-capability-history/reader.js +97 -0
- package/apps/bridge/src/sessions/scan/claude/activity.ts +10 -3
- package/apps/bridge/src/sessions/scan/claude/index.ts +1 -0
- package/apps/bridge/src/sessions/scan/claude/parse.ts +6 -2
- package/apps/bridge/src/sessions/scan/claude/recorded-usage/README.md +17 -0
- package/apps/bridge/src/sessions/scan/claude/recorded-usage/index.ts +60 -0
- package/apps/bridge/src/sessions/scan/codex/activity.ts +15 -5
- package/apps/bridge/src/sessions/scan/grok.ts +12 -2
- package/apps/bridge/src/sessions/support/capability-totals.ts +0 -0
- package/apps/bridge/src/sessions/telemetry/execution-summaries.ts +56 -0
- package/apps/mcp/src/desktop/task-activity-worker.ts +4 -4
- package/apps/mcp/src/http/README.md +15 -0
- package/apps/mcp/src/http/http-server.ts +1 -3
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/core/relay-client-types.ts +6 -0
- package/packages/core/relay-client.ts +14 -19
- package/packages/core/relay-envelope.ts +7 -3
- package/packages/core/relay-inbound/index.ts +2 -0
- package/packages/core/relay-inbound/wait-for.ts +19 -0
- package/packages/core/retained-state/README.md +23 -0
- package/packages/core/retained-state/index.ts +25 -0
- package/packages/protocol/messages/capabilities.ts +17 -0
- 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 {
|
|
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:
|
|
21
|
+
inspect: (cwd: string) => RepositoryFacts | Promise<RepositoryFacts>;
|
|
22
22
|
sync: typeof queueProjectBindingSync;
|
|
23
|
-
} = { endpoint: computerId, inspect:
|
|
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.
|
|
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:
|
|
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 {
|
|
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
|
|
118
|
+
clocks: { lastHistoryPublishedAt: number };
|
|
118
119
|
loadLoop: { updateStatus: (status: SessionsStatus) => void };
|
|
119
120
|
forceHistory: boolean;
|
|
120
121
|
active: () => boolean;
|
|
121
|
-
}): Promise<{ lastHistoryPublishedAt: number
|
|
122
|
+
}): Promise<{ lastHistoryPublishedAt: number }> {
|
|
122
123
|
const { client, leadership, subscriptions, history, clocks, loadLoop, forceHistory } = input;
|
|
123
|
-
let { lastHistoryPublishedAt
|
|
124
|
-
if (!client.isConnected) return { lastHistoryPublishedAt
|
|
124
|
+
let { lastHistoryPublishedAt } = clocks;
|
|
125
|
+
if (!client.isConnected) return { lastHistoryPublishedAt };
|
|
125
126
|
await publishHeartbeat(client).catch(() => {});
|
|
126
|
-
if (!input.active()) return { lastHistoryPublishedAt
|
|
127
|
-
if (!leadership.acquire()) return { lastHistoryPublishedAt
|
|
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
|
-
|
|
150
|
-
|
|
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 =
|
|
193
|
-
if (
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|