granttap-mcp 0.8.86 → 0.8.88
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 +13 -1
- package/apps/bridge/src/host/session-keys.ts +5 -1
- package/apps/bridge/src/monitor/capability-publication/index.ts +9 -4
- package/apps/bridge/src/monitor/handlers/user-message.ts +12 -9
- package/apps/bridge/src/monitor/support/runtime.ts +8 -5
- package/apps/bridge/src/reply/grok-response/README.md +20 -0
- package/apps/bridge/src/reply/grok-response/index.ts +48 -0
- package/apps/bridge/src/reply/process/index.ts +14 -1
- package/apps/bridge/src/reply/process/provider-headless.ts +3 -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
|
@@ -155,7 +155,8 @@ device pairing without a passkey account.
|
|
|
155
155
|
|
|
156
156
|
- Claude Code and Codex: primary local control and continuation paths.
|
|
157
157
|
- Cursor (Beta): Task visibility and supported local controls, with narrower coverage.
|
|
158
|
-
- Grok Build:
|
|
158
|
+
- Grok Build: local history and headless follow-ups through an installed,
|
|
159
|
+
authenticated CLI; provider hook and scoped Mesh coverage remain narrower.
|
|
159
160
|
|
|
160
161
|
GrantTap reports the depth each provider actually exposes. Visibility does not
|
|
161
162
|
imply deterministic remote blocking or full mobile continuation.
|
|
@@ -477,6 +478,13 @@ grok plugin marketplace add sergii-ziborov/granttap-mcp
|
|
|
477
478
|
grok plugin install granttap --trust
|
|
478
479
|
```
|
|
479
480
|
|
|
481
|
+
In Grok, open `/mcps` and authenticate the GrantTap server, then run
|
|
482
|
+
`/granttap-connect`. GrantTap's phone pairing and Grok's permission to use that
|
|
483
|
+
MCP server are separate. An existing pairing does not authorize a new MCP client.
|
|
484
|
+
If a Grok session does not start responding within one minute, GrantTap asks you
|
|
485
|
+
to inspect the original task before retrying. A turn that has started can keep
|
|
486
|
+
running; its later failure does not mean that the message was never delivered.
|
|
487
|
+
|
|
480
488
|
For Cursor, use the reviewed **GrantTap** Marketplace listing when it is visible
|
|
481
489
|
to your account. Reload Cursor. On this computer, run `granttap setup`. Pair
|
|
482
490
|
and change settings on [granttap.com/connect](https://granttap.com/connect)
|
|
@@ -843,3 +851,7 @@ sustained structured work in a different checkout without interpreting chat
|
|
|
843
851
|
titles. Task identity and its original coordination/access scope survive.
|
|
844
852
|
|
|
845
853
|
See [repository observations](apps/bridge/src/mesh/repository-details/README.md).
|
|
854
|
+
|
|
855
|
+
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.
|
|
856
|
+
|
|
857
|
+
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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setImmediate } from "node:timers/promises";
|
|
2
2
|
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
3
3
|
import type { SessionInfo, SessionsStatus } from "../../../../../packages/protocol/schema";
|
|
4
|
-
import {
|
|
4
|
+
import { scanRecordedCapabilityUsage, scopeCapabilityUsageToRoom } from "../../sessions";
|
|
5
5
|
|
|
6
6
|
type Publication = { running: boolean; closed: boolean; lastPublishedAt: number };
|
|
7
7
|
const publications = new WeakMap<RelayClient, Publication>();
|
|
@@ -16,11 +16,16 @@ export function publishMonitorCapabilities(client: RelayClient, status: Sessions
|
|
|
16
16
|
const work = (async () => {
|
|
17
17
|
await setImmediate();
|
|
18
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);
|
|
19
26
|
const older = await history();
|
|
20
27
|
if (state.closed || !client.isConnected) return;
|
|
21
|
-
|
|
22
|
-
await client.send(scopeCapabilityUsageToRoom(usage, client.room), "phone",
|
|
23
|
-
{ ttlMs: 90_000, reliable: false });
|
|
28
|
+
if (older.length) await publish([...status.sessions, ...older]);
|
|
24
29
|
state.lastPublishedAt = Date.now();
|
|
25
30
|
})();
|
|
26
31
|
void work.catch(() => {}).finally(() => { state.running = false; });
|
|
@@ -98,20 +98,20 @@ async function startNewAgentTask(input: {
|
|
|
98
98
|
}
|
|
99
99
|
const settle = async (result: ReplyResult, late: boolean) => {
|
|
100
100
|
input.origin?.registry.complete(input.origin.scope,
|
|
101
|
-
{ ok: result.ok, sessionId: result.ok ? result.sessionId : undefined });
|
|
102
|
-
if (!result.ok && message.messageId) {
|
|
101
|
+
{ ok: result.ok || result.delivery === 'native', sessionId: result.ok ? result.sessionId : undefined });
|
|
102
|
+
if (!result.ok && message.messageId && result.delivery !== 'native') {
|
|
103
103
|
if (late) rejectCompletedDelivery(message.messageId, result.error);
|
|
104
104
|
await sendDeliveryReceipt(client, message.messageId, 'rejected', result.error,
|
|
105
105
|
undefined, input.peerPublicKey);
|
|
106
106
|
}
|
|
107
107
|
return result.ok ? say(result.text, result.sessionId, true)
|
|
108
|
-
: say(late ? `The ${displayName} task stopped: ${result.error}`
|
|
108
|
+
: say(late || result.delivery === 'native' ? `The ${displayName} task stopped: ${result.error}`
|
|
109
109
|
: `Could not create a ${displayName} task: ${result.error}`, undefined, true);
|
|
110
110
|
};
|
|
111
111
|
const early = await acceptWhenStarted(run, (result) => settle(result, true));
|
|
112
112
|
if (early === "started") return "accepted";
|
|
113
113
|
await settle(early, false);
|
|
114
|
-
return early.ok ? "accepted" : "rejected";
|
|
114
|
+
return early.ok || early.delivery === 'native' ? "accepted" : "rejected";
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
export async function handleUserMessage(
|
|
@@ -196,15 +196,18 @@ export async function handleUserMessage(
|
|
|
196
196
|
}
|
|
197
197
|
if (result.ok) await say(result.text, result.sessionId ?? target.sessionId, true);
|
|
198
198
|
else {
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
if (result.delivery !== 'native') {
|
|
200
|
+
if (late && message.messageId) rejectCompletedDelivery(message.messageId, result.error);
|
|
201
|
+
if (message.messageId) await sendDeliveryReceipt(client, message.messageId, 'rejected',
|
|
202
|
+
result.error, target.sessionId, peerPublicKey);
|
|
203
|
+
}
|
|
204
|
+
await say(late || result.delivery === 'native' ? `The run stopped: ${result.error}`
|
|
205
|
+
: `Could not deliver the message: ${result.error}`,
|
|
203
206
|
target.sessionId, true);
|
|
204
207
|
}
|
|
205
208
|
};
|
|
206
209
|
const early = await acceptWhenStarted(run, (result) => settle(result, true));
|
|
207
210
|
if (early === "started") return "accepted";
|
|
208
211
|
await settle(early, false);
|
|
209
|
-
return early.ok ? "accepted" : "rejected";
|
|
212
|
+
return early.ok || early.delivery === 'native' ? "accepted" : "rejected";
|
|
210
213
|
}
|
|
@@ -128,17 +128,20 @@ async function publishMonitorTick(input: {
|
|
|
128
128
|
if (!leadership.acquire()) return { lastHistoryPublishedAt };
|
|
129
129
|
await client.send(approvalsStatus(), "phone", { ttlMs: INTERVAL_MS * 3, reliable: false });
|
|
130
130
|
const initial = lastHistoryPublishedAt === 0;
|
|
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 };
|
|
135
|
+
resumedHeads.add(client);
|
|
136
|
+
void resumePublishedChats(client,
|
|
137
|
+
(session, saved) => publishSessionEvents(client, session.sessionId, saved)).catch(() => {});
|
|
138
|
+
}
|
|
131
139
|
const includeHistory = forceHistory || (!initial && Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS);
|
|
132
140
|
const snapshot = await monitorSnapshot({ includeHistory, forceHistory, subscriptions, history });
|
|
133
141
|
if (!input.active()) return { lastHistoryPublishedAt };
|
|
134
142
|
const status = decorateTaskOriginCatalog(snapshot, client.room);
|
|
135
143
|
rememberPublishedCatalog(client, status);
|
|
136
144
|
await client.send(status, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
|
|
137
|
-
if (initial && !resumedHeads.has(client)) {
|
|
138
|
-
resumedHeads.add(client);
|
|
139
|
-
void resumePublishedChats(client,
|
|
140
|
-
(session, saved) => publishSessionEvents(client, session.sessionId, saved)).catch(() => {});
|
|
141
|
-
}
|
|
142
145
|
// Provider review is independent of task visibility; a hung hook RPC must
|
|
143
146
|
// never hold the current catalog behind its discovery timeout.
|
|
144
147
|
void refreshCodexHooks().catch(() => {});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Grok response admission
|
|
2
|
+
|
|
3
|
+
[`index.ts`](index.ts) reads Grok Build's `streaming-json` response contract.
|
|
4
|
+
Actual text, thinking, or a started tool call confirms that the prompt reached
|
|
5
|
+
execution. Startup metadata alone does not establish admission. Native error
|
|
6
|
+
messages remain failures and metadata is never returned as answer text.
|
|
7
|
+
|
|
8
|
+
Opening a session has a 60-second response-activity deadline, separate from the
|
|
9
|
+
long execution budget. A stalled opening stops only the process group GrantTap
|
|
10
|
+
started and asks the person to inspect the native task before retrying. Once
|
|
11
|
+
response activity is witnessed, a later timeout or run failure cannot rewrite
|
|
12
|
+
the accepted message as a rejected delivery. Execution failure still reaches
|
|
13
|
+
the chat. This does not grant tools, authenticate MCP, or alter Grok policy.
|
|
14
|
+
|
|
15
|
+
Behavior tests live in `../tests/grok-response.test.ts` and
|
|
16
|
+
`../tests/admission-deadline.test.ts`; durable receipt tests live in
|
|
17
|
+
`../../monitor/message-delivery/tests/run-outcome.test.ts`.
|
|
18
|
+
|
|
19
|
+
License: this module is distributed under the GrantTap Commercial Source License
|
|
20
|
+
in the repository-root `LICENSE` file.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReplyResult } from '../payload/types';
|
|
2
|
+
import type { ProviderAdmission } from '../process';
|
|
3
|
+
|
|
4
|
+
function events(stdout: string): Record<string, unknown>[] {
|
|
5
|
+
return stdout.split('\n').flatMap(line => {
|
|
6
|
+
try {
|
|
7
|
+
const value: unknown = JSON.parse(line);
|
|
8
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
9
|
+
? [value as Record<string, unknown>] : [];
|
|
10
|
+
} catch { return []; }
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Response activity proves admission; startup lists and diagnostic text do not. */
|
|
15
|
+
export function grokAccepted(stdout: string): boolean {
|
|
16
|
+
return events(stdout).some(event => {
|
|
17
|
+
if (['text', 'thought'].includes(String(event.type))) {
|
|
18
|
+
return typeof event.data === 'string' && !!event.data.trim();
|
|
19
|
+
}
|
|
20
|
+
if (event.type === 'result') return typeof event.result === 'string' && !!event.result.trim();
|
|
21
|
+
return event.type === 'tool_call' && event.status === 'in_progress'
|
|
22
|
+
&& typeof event.toolCallId === 'string' && !!event.toolCallId;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Opening a native session has a separate limit from executing its long turn. */
|
|
27
|
+
export const grokAdmission: ProviderAdmission = {
|
|
28
|
+
confirmed: grokAccepted,
|
|
29
|
+
withinMs: 60_000,
|
|
30
|
+
timeoutMessage: 'Grok did not confirm receipt within 60s. Open the task before retrying.',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function parseGrok(stdout: string, fallback: string): ReplyResult {
|
|
34
|
+
let text = '';
|
|
35
|
+
let sessionId = fallback;
|
|
36
|
+
for (const event of events(stdout)) {
|
|
37
|
+
if (event.type === 'text' && typeof event.data === 'string') text += event.data;
|
|
38
|
+
if (event.type === 'result' && typeof event.result === 'string') text = event.result;
|
|
39
|
+
if (typeof event.sessionId === 'string') sessionId = event.sessionId;
|
|
40
|
+
if (typeof event.session_id === 'string') sessionId = event.session_id;
|
|
41
|
+
const error = typeof event.message === 'string' ? event.message : event.error;
|
|
42
|
+
if (event.type === 'error' && typeof error === 'string') {
|
|
43
|
+
return { ok: false, error: error.slice(0, 500) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return text.trim() ? { ok: true, text: text.trim().slice(0, 4_000), sessionId }
|
|
47
|
+
: { ok: false, error: 'Grok Build returned an empty response.' };
|
|
48
|
+
}
|
|
@@ -72,6 +72,12 @@ export function runningProcessCount(key: string): number {
|
|
|
72
72
|
return running.get(key)?.size ?? 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export type ProviderAdmission = {
|
|
76
|
+
confirmed: (stdout: string) => boolean;
|
|
77
|
+
withinMs: number;
|
|
78
|
+
timeoutMessage: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
75
81
|
export function runProcess(
|
|
76
82
|
command: string,
|
|
77
83
|
args: string[],
|
|
@@ -80,6 +86,7 @@ export function runProcess(
|
|
|
80
86
|
parse: (stdout: string) => ReplyResult,
|
|
81
87
|
stdin?: string,
|
|
82
88
|
key?: string,
|
|
89
|
+
admission?: ProviderAdmission,
|
|
83
90
|
): Promise<ReplyResult> {
|
|
84
91
|
return new Promise((resolve) => {
|
|
85
92
|
let child: ChildProcess;
|
|
@@ -104,14 +111,20 @@ export function runProcess(
|
|
|
104
111
|
if (done) return;
|
|
105
112
|
done = true;
|
|
106
113
|
clearTimeout(timer);
|
|
114
|
+
clearTimeout(startupTimer);
|
|
107
115
|
untrack();
|
|
108
|
-
resolve(result);
|
|
116
|
+
resolve(!result.ok && admission?.confirmed(stdout) ? { ...result, delivery: 'native' } : result);
|
|
109
117
|
};
|
|
110
118
|
const timer = setTimeout(() => {
|
|
111
119
|
if (done) return;
|
|
112
120
|
signalOwnedProcess(child, "SIGKILL");
|
|
113
121
|
finish({ ok: false, error: `${command} did not respond within ${Math.round(timeoutMs / 1000)}s.` });
|
|
114
122
|
}, timeoutMs);
|
|
123
|
+
const startupTimer = admission ? setTimeout(() => {
|
|
124
|
+
if (done || admission.confirmed(stdout)) return;
|
|
125
|
+
signalOwnedProcess(child, "SIGKILL");
|
|
126
|
+
finish({ ok: false, error: admission.timeoutMessage });
|
|
127
|
+
}, admission.withinMs) : undefined;
|
|
115
128
|
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
116
129
|
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
117
130
|
child.on("error", (error) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { grokAdmission, parseGrok } from "../grok-response";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import type { SessionInfo } from "../../../../../packages/protocol/schema";
|
|
3
4
|
import { runProcess } from ".";
|
|
@@ -11,33 +12,6 @@ function grokBin(): string {
|
|
|
11
12
|
return resolveGrokBinary();
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
function parseGrok(stdout: string, fallback: string): ReplyResult {
|
|
15
|
-
let text = "";
|
|
16
|
-
let sessionId = fallback;
|
|
17
|
-
for (const line of stdout.split("\n")) {
|
|
18
|
-
if (!line.trim()) continue;
|
|
19
|
-
try {
|
|
20
|
-
const value = JSON.parse(line) as {
|
|
21
|
-
type?: string; data?: unknown; sessionId?: unknown; session_id?: unknown;
|
|
22
|
-
result?: unknown; error?: unknown;
|
|
23
|
-
};
|
|
24
|
-
if (value.type === "text" && typeof value.data === "string") text += value.data;
|
|
25
|
-
if (value.type === "result" && typeof value.result === "string") text = value.result;
|
|
26
|
-
if (typeof value.sessionId === "string") sessionId = value.sessionId;
|
|
27
|
-
if (typeof value.session_id === "string") sessionId = value.session_id;
|
|
28
|
-
if (value.type === "error" && typeof value.error === "string") {
|
|
29
|
-
return { ok: false, error: value.error.slice(0, 500) };
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
// Ignore diagnostic lines outside Grok's streaming JSON contract.
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const visible = text.trim() || stdout.trim();
|
|
36
|
-
return visible
|
|
37
|
-
? { ok: true, text: visible.slice(0, 4_000), sessionId }
|
|
38
|
-
: { ok: false, error: "Grok Build returned an empty response." };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
15
|
export function runCursorNew(
|
|
42
16
|
text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string, effort?: string,
|
|
43
17
|
): Promise<ReplyResult> {
|
|
@@ -62,7 +36,7 @@ export function runGrokNew(
|
|
|
62
36
|
"-p", text, "--output-format", "streaming-json",
|
|
63
37
|
];
|
|
64
38
|
return runProcess(grokBin(), args, cwd, timeoutMs, (stdout) => parseGrok(stdout, sessionId),
|
|
65
|
-
undefined, operationKey);
|
|
39
|
+
undefined, operationKey, grokAdmission);
|
|
66
40
|
}
|
|
67
41
|
|
|
68
42
|
export function runGrokResume(
|
|
@@ -79,6 +53,6 @@ export function runGrokResume(
|
|
|
79
53
|
return runProcess(
|
|
80
54
|
grokBin(), args, session.cwd, timeoutMs,
|
|
81
55
|
(stdout) => parseGrok(stdout, session.sessionId),
|
|
82
|
-
undefined, session.sessionId,
|
|
56
|
+
undefined, session.sessionId, grokAdmission,
|
|
83
57
|
);
|
|
84
58
|
}
|
|
@@ -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
|
+
}
|