granttap-mcp 0.8.46 → 0.8.48
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 +6 -0
- package/apps/bridge/src/config/index.ts +1 -0
- package/apps/bridge/src/engine/README.md +2 -1
- package/apps/bridge/src/engine/runtime/engine-projects.ts +9 -4
- package/apps/bridge/src/mesh/runtime/graph/analysis.ts +69 -0
- package/apps/bridge/src/mesh/runtime/index.ts +26 -96
- package/apps/bridge/src/mesh/runtime/snapshot/compose.ts +45 -0
- package/apps/bridge/src/mesh/runtime/snapshot/enriched.ts +2 -1
- package/apps/bridge/src/monitor/README.md +13 -0
- package/apps/bridge/src/monitor/graph-refresh.ts +7 -4
- package/apps/bridge/src/monitor/handlers/catalog.ts +5 -2
- package/apps/bridge/src/monitor/handlers/inbound.ts +2 -2
- package/apps/bridge/src/monitor/mesh-publication/README.md +9 -0
- package/apps/bridge/src/monitor/mesh-publication/index.ts +45 -0
- package/apps/bridge/src/monitor/support/runtime.ts +11 -34
- package/apps/bridge/src/session-catalog/README.md +16 -0
- package/apps/bridge/src/session-catalog/index.ts +70 -0
- package/apps/bridge/src/session-images/README.md +5 -0
- package/apps/bridge/src/session-images/index.ts +3 -1
- package/apps/bridge/src/sessions/index.ts +1 -0
- package/apps/bridge/src/sessions/native-source.ts +35 -0
- package/apps/bridge/src/transcript-history/images/native.ts +13 -8
- package/apps/bridge/src/transcript-history/images.ts +5 -3
- package/apps/mcp/src/desktop/image/index.ts +4 -1
- 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/protocol/messages/interaction/index.ts +1 -0
- package/packages/protocol/messages/mesh/snapshot.ts +1 -0
package/README.md
CHANGED
|
@@ -17,6 +17,12 @@ adapters, and wire contracts. It powers Project Mesh, Governance, and observed
|
|
|
17
17
|
usage while agents and provider credentials stay on your computer. Controller
|
|
18
18
|
traffic is end-to-end encrypted.
|
|
19
19
|
|
|
20
|
+
Known chats are read from their published native identities, without another
|
|
21
|
+
walk through every provider log. The local helper retains a private metadata
|
|
22
|
+
index so a restart can send current heads of the saved Now chats before a fresh
|
|
23
|
+
discovery scan. The Apple clients keep downloaded conversations, pictures, and
|
|
24
|
+
Mesh data locally for offline use; clearing that cache preserves the account login.
|
|
25
|
+
|
|
20
26
|
The MCP runtime, plugins, and CLI are open source under the MIT License.
|
|
21
27
|
The Apple apps, Engine, relay, and website are separately licensed commercial
|
|
22
28
|
products or source. Their licenses do not change this repository's MIT grant.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Public entry point for local bridge configuration and pairing. */
|
|
2
2
|
export * from "./access/capability-policy";
|
|
3
3
|
export * from "./access/pairing";
|
|
4
|
+
export { writePrivateFile } from "./access/write-private";
|
|
4
5
|
export * from "./runtime/paths";
|
|
5
6
|
export * from "./runtime";
|
|
6
7
|
export {
|
|
@@ -14,7 +14,8 @@ Mesh snapshot enrichment queues Weavatrix repository analysis through
|
|
|
14
14
|
`repository-graph-jobs.ts`. One analysis runs at a time, repeated snapshot
|
|
15
15
|
ticks reuse a fresh bounded report, and failed analyses back off. An absent
|
|
16
16
|
report leaves the graph unavailable and Cortex degraded; it does not block
|
|
17
|
-
policy IPC
|
|
17
|
+
policy IPC: repository analysis owns a separate bounded-lifetime socket, while
|
|
18
|
+
policy and Backbone requests keep their control connection. It does not turn a repository label into an architecture result.
|
|
18
19
|
Claude, Codex, and Cursor hooks can evaluate the engine's local Project
|
|
19
20
|
Governance policy only when both `GRANTTAP_ENGINE_ENABLED` and
|
|
20
21
|
`GRANTTAP_PROJECT_POLICY_ENABLED` are enabled. Project `DENY` and `ASK` precede
|
|
@@ -182,7 +182,7 @@ export async function projectRepositoryGraphs(
|
|
|
182
182
|
priority?: number } = {},
|
|
183
183
|
): Promise<ProjectRepositoryGraph[]> {
|
|
184
184
|
if (!engineFeatureEnabled(options.env ?? process.env)) return [];
|
|
185
|
-
const client = options.client
|
|
185
|
+
const client = options.client;
|
|
186
186
|
const repositories = [...new Map(bindings.map((item) => [item.repositoryId, item])).values()].slice(0, 64);
|
|
187
187
|
const graphs = options.background
|
|
188
188
|
? repositories.map((binding) => repositoryGraphJobs.read(
|
|
@@ -191,7 +191,7 @@ export async function projectRepositoryGraphs(
|
|
|
191
191
|
options.priority ?? 0,
|
|
192
192
|
))
|
|
193
193
|
: await Promise.all(repositories.map((binding) =>
|
|
194
|
-
analyzeRepositoryGraph(client, projectId, binding.repositoryId,
|
|
194
|
+
analyzeRepositoryGraph(client, projectId, binding.repositoryId, 120_000).then((graph) => {
|
|
195
195
|
repositoryGraphJobs.remember(JSON.stringify([projectId, binding.repositoryId, binding.revision]), graph);
|
|
196
196
|
return graph;
|
|
197
197
|
})));
|
|
@@ -199,10 +199,13 @@ export async function projectRepositoryGraphs(
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
async function analyzeRepositoryGraph(
|
|
202
|
-
client: EngineClientLike, projectId: string, repositoryId: string, timeoutMs: number,
|
|
202
|
+
client: EngineClientLike | undefined, projectId: string, repositoryId: string, timeoutMs: number,
|
|
203
203
|
): Promise<ProjectRepositoryGraph | undefined> {
|
|
204
|
+
// The daemon serves a socket serially. Analysis must not occupy the channel
|
|
205
|
+
// used for policy, bindings and Backbone reads while a repository is scanned.
|
|
206
|
+
const channel = client ?? new EngineClient({ socketPath: join(configDir(), "engine.sock") });
|
|
204
207
|
try {
|
|
205
|
-
const result = await
|
|
208
|
+
const result = await channel.request({
|
|
206
209
|
operation: "graph.analyze_repository", input: {
|
|
207
210
|
project_id: projectId, repository_id: repositoryId,
|
|
208
211
|
},
|
|
@@ -251,6 +254,8 @@ async function analyzeRepositoryGraph(
|
|
|
251
254
|
analysisStatus: "UNAVAILABLE", analysisErrorCode: code,
|
|
252
255
|
nodes: [], relations: [], totalNodes: 0, totalRelations: 0, truncated: false,
|
|
253
256
|
};
|
|
257
|
+
} finally {
|
|
258
|
+
if (!client) channel.close();
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { RelayClient } from "../../../../../../packages/core/relay-client";
|
|
2
|
+
import type { MeshSnapshot } from "../../../../../../packages/protocol/schema";
|
|
3
|
+
import { projectBackbone, projectRepositoryGraphs, waitForProjectBindingSync } from "../../../engine/runtime/engine-projects";
|
|
4
|
+
import { sendMeshPayload } from "../../../host/session-keys";
|
|
5
|
+
import { publishedCatalog } from "../../../session-catalog";
|
|
6
|
+
import { meshSnapshot, meshSnapshots } from "..";
|
|
7
|
+
import { refreshLocalGraphBindings } from "./binding-sync";
|
|
8
|
+
|
|
9
|
+
/** A person's explicit Graph refresh bypasses background job backoff. */
|
|
10
|
+
type GraphAnalysisDependencies = {
|
|
11
|
+
snapshot?: (projectId: string) => MeshSnapshot | undefined;
|
|
12
|
+
snapshots?: typeof meshSnapshots;
|
|
13
|
+
wait: typeof waitForProjectBindingSync;
|
|
14
|
+
sync?: typeof refreshLocalGraphBindings;
|
|
15
|
+
backbone: typeof projectBackbone;
|
|
16
|
+
graphs: typeof projectRepositoryGraphs;
|
|
17
|
+
send: (client: RelayClient, snapshot: MeshSnapshot) => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const graphAnalysisDependencies: GraphAnalysisDependencies = {
|
|
21
|
+
snapshot: meshSnapshot,
|
|
22
|
+
wait: waitForProjectBindingSync,
|
|
23
|
+
sync: refreshLocalGraphBindings,
|
|
24
|
+
backbone: projectBackbone,
|
|
25
|
+
graphs: projectRepositoryGraphs,
|
|
26
|
+
send: (client, snapshot) => sendMeshPayload(client, snapshot, "phone", {
|
|
27
|
+
ttlMs: 60_000, reliable: true,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function analyzeProjectGraphNow(
|
|
32
|
+
client: RelayClient, projectId: string,
|
|
33
|
+
dependencies: GraphAnalysisDependencies = graphAnalysisDependencies,
|
|
34
|
+
requestIds?: ReadonlySet<string>,
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
const known = publishedCatalog(client);
|
|
37
|
+
const read = dependencies.snapshot ?? ((id: string) => dependencies.snapshots?.().find(item => item.projectId === id));
|
|
38
|
+
const snapshot = dependencies === graphAnalysisDependencies
|
|
39
|
+
? meshSnapshot(projectId, known ? [...known.sessions, ...(known.history ?? [])] : undefined)
|
|
40
|
+
: read(projectId);
|
|
41
|
+
if (!snapshot) return false;
|
|
42
|
+
await dependencies.wait(projectId);
|
|
43
|
+
const current = dependencies === graphAnalysisDependencies
|
|
44
|
+
? meshSnapshot(projectId, known ? [...known.sessions, ...(known.history ?? [])] : undefined)
|
|
45
|
+
: read(projectId);
|
|
46
|
+
if (!current) return false;
|
|
47
|
+
const failures = dependencies.sync ? await dependencies.sync(current) : [];
|
|
48
|
+
const rejected = new Set(failures.map(report => report.repositoryId));
|
|
49
|
+
const [backbone, repositoryGraphs] = await Promise.all([
|
|
50
|
+
dependencies.backbone(projectId),
|
|
51
|
+
dependencies.graphs(projectId, (current.bindings ?? []).filter(binding => !rejected.has(binding.repositoryId)), { background: false }),
|
|
52
|
+
]);
|
|
53
|
+
const observed = [...repositoryGraphs, ...failures];
|
|
54
|
+
const reports = observed.length > 0 ? observed : [{
|
|
55
|
+
projectId, repositoryId: current.project.canonicalRepositoryId,
|
|
56
|
+
revision: "unverified", weavatrixVersion: "unknown",
|
|
57
|
+
analysisStatus: "UNAVAILABLE" as const, analysisErrorCode: "REPOSITORY_NOT_BOUND",
|
|
58
|
+
nodes: [], relations: [], totalNodes: 0, totalRelations: 0, truncated: false,
|
|
59
|
+
}];
|
|
60
|
+
const receipts = requestIds?.size ? requestIds : new Set<string | undefined>([undefined]);
|
|
61
|
+
for (const id of receipts) {
|
|
62
|
+
await dependencies.send(client, {
|
|
63
|
+
...current, backbone, repositoryGraphs: reports, generatedAt: Date.now(),
|
|
64
|
+
...(id ? { graphAnalysisRequestId: id } : {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -10,9 +10,6 @@ import type {
|
|
|
10
10
|
SessionInfo,
|
|
11
11
|
TaskCapsule,
|
|
12
12
|
} from "../../../../../packages/protocol/schema";
|
|
13
|
-
import {
|
|
14
|
-
projectBackbone, projectRepositoryGraphs, waitForProjectBindingSync,
|
|
15
|
-
} from "../../engine/runtime/engine-projects";
|
|
16
13
|
import { configDir } from "../../config";
|
|
17
14
|
import {
|
|
18
15
|
createClaudeSession,
|
|
@@ -25,10 +22,7 @@ import { isProviderEnabled } from "../../config/runtime";
|
|
|
25
22
|
import { sendMeshPayload } from "../../host/session-keys";
|
|
26
23
|
import { linkSessionsToProjects, workingTreeState } from "../catalog";
|
|
27
24
|
import { catalogFromEndpoint } from "../catalog/models";
|
|
28
|
-
import { projectMcpServers } from "../catalog/project/capabilities";
|
|
29
|
-
import { configuredProjectMcpServers, mergeConfiguredMcpServers } from "../catalog/project/configured-mcp";
|
|
30
25
|
import { saveProjectCapabilityRequest } from "../catalog/project/requests";
|
|
31
|
-
import { projectCapabilityObservations } from "../catalog/project/observations";
|
|
32
26
|
import { computerId } from "../identity/computer";
|
|
33
27
|
import { createCheckpoint } from "../admin/checkpoint";
|
|
34
28
|
import { buildTaskCapsule } from "../admin/capsule";
|
|
@@ -36,11 +30,11 @@ import { handoffReadiness } from "../tasks/readiness";
|
|
|
36
30
|
import { createHandoffFlow } from "./handoff";
|
|
37
31
|
import type { MeshRuntimeDependencies } from "./dependencies";
|
|
38
32
|
import { localMeshStore } from "../local-remote/local";
|
|
33
|
+
import { composeMeshSnapshot } from "./snapshot/compose";
|
|
39
34
|
import { enrichMeshSnapshot } from "./snapshot/enriched";
|
|
40
35
|
import { createHandoffWorktree, repositoryHasCommit } from "./worktree";
|
|
41
36
|
import { fetchRevision, pushBranch } from "../local-remote/remote";
|
|
42
|
-
import { projectSessionCapabilityInventory
|
|
43
|
-
import { refreshLocalGraphBindings } from "./graph/binding-sync";
|
|
37
|
+
import { projectSessionCapabilityInventory } from "./capabilities";
|
|
44
38
|
import { createMeshSessionCatalog } from "./catalog/session-catalog";
|
|
45
39
|
|
|
46
40
|
export type { MeshRuntimeDependencies };
|
|
@@ -158,53 +152,29 @@ export function createMeshRuntime(deps: MeshRuntimeDependencies) {
|
|
|
158
152
|
const catalog = (sessions: SessionInfo[]) =>
|
|
159
153
|
linkSessionsToProjects(deps.store(), sessions, deps.computer());
|
|
160
154
|
|
|
161
|
-
const composeSnapshot = (
|
|
162
|
-
projectId: string,
|
|
163
|
-
store = deps.store(),
|
|
164
|
-
sessions = deps.sessions(),
|
|
165
|
-
models = catalogFromEndpoint(deps.computer(), sessions),
|
|
166
|
-
): MeshSnapshot | undefined => {
|
|
167
|
-
const snapshot = store.snapshot(projectId, deps.computer());
|
|
168
|
-
if (!snapshot) return undefined;
|
|
169
|
-
const linked = projectExecutionCapabilitySessions(
|
|
170
|
-
snapshot, sessions, deps.computer(), deps.capabilityInventory,
|
|
171
|
-
);
|
|
172
|
-
const reported = projectMcpServers(
|
|
173
|
-
linked.filter((session) => session.computerId === deps.computer()), projectId,
|
|
174
|
-
);
|
|
175
|
-
const workspaces = (snapshot.bindings ?? [])
|
|
176
|
-
.filter((binding) => binding.endpointId === deps.computer()
|
|
177
|
-
&& binding.available && binding.localPathHint)
|
|
178
|
-
.map((binding) => binding.localPathHint!);
|
|
179
|
-
const mcpServers = mergeConfiguredMcpServers(
|
|
180
|
-
configuredProjectMcpServers(deps.computer(), workspaces), reported,
|
|
181
|
-
);
|
|
182
|
-
const capabilityObservations = projectCapabilityObservations({
|
|
183
|
-
projectId, endpointId: deps.computer(), bound: workspaces.length > 0,
|
|
184
|
-
requests: snapshot.capabilityRequests ?? [], skills: snapshot.skills ?? [],
|
|
185
|
-
mcpServers, now: deps.now(),
|
|
186
|
-
});
|
|
187
|
-
return {
|
|
188
|
-
...snapshot,
|
|
189
|
-
publisherEndpointId: deps.computer(),
|
|
190
|
-
mcpServers: mcpServers.length > 0 ? mcpServers : undefined,
|
|
191
|
-
capabilityObservations: capabilityObservations.length > 0
|
|
192
|
-
? capabilityObservations : undefined,
|
|
193
|
-
modelCatalog: models.models.length > 0 || models.reason ? [models] : undefined,
|
|
194
|
-
};
|
|
195
|
-
};
|
|
196
155
|
|
|
197
156
|
return {
|
|
198
157
|
catalog,
|
|
199
|
-
snapshot(projectId: string): MeshSnapshot | undefined {
|
|
200
|
-
return
|
|
158
|
+
snapshot(projectId: string, sessions?: SessionInfo[]): MeshSnapshot | undefined {
|
|
159
|
+
return composeMeshSnapshot(deps, projectId, deps.store(), sessions ?? deps.sessions());
|
|
201
160
|
},
|
|
202
161
|
snapshots(): MeshSnapshot[] {
|
|
203
162
|
const store = deps.store();
|
|
204
163
|
const sessions = deps.sessions();
|
|
205
164
|
const models = catalogFromEndpoint(deps.computer(), sessions);
|
|
206
165
|
return store.projectIds().flatMap((projectId) =>
|
|
207
|
-
|
|
166
|
+
composeMeshSnapshot(deps, projectId, store, sessions, models) ?? []);
|
|
167
|
+
},
|
|
168
|
+
async snapshotsAsync(sessions = deps.sessions()): Promise<MeshSnapshot[]> {
|
|
169
|
+
const store = deps.store();
|
|
170
|
+
const models = catalogFromEndpoint(deps.computer(), sessions);
|
|
171
|
+
const snapshots: MeshSnapshot[] = [];
|
|
172
|
+
for (const id of store.projectIds()) {
|
|
173
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
174
|
+
const snapshot = composeMeshSnapshot(deps, id, store, sessions, models);
|
|
175
|
+
if (snapshot) snapshots.push(snapshot);
|
|
176
|
+
}
|
|
177
|
+
return snapshots;
|
|
208
178
|
},
|
|
209
179
|
requestCapability(input: ProjectCapabilityRequestSet): boolean {
|
|
210
180
|
const project = deps.store().snapshot(input.projectId)?.project;
|
|
@@ -251,16 +221,21 @@ export function meshSnapshots(): MeshSnapshot[] {
|
|
|
251
221
|
return defaultRuntime.snapshots();
|
|
252
222
|
}
|
|
253
223
|
|
|
254
|
-
|
|
255
|
-
|
|
224
|
+
/** Yield between Project projections so live decisions and heartbeats continue. */
|
|
225
|
+
export function meshSnapshotsAsync(sessions?: SessionInfo[]): Promise<MeshSnapshot[]> {
|
|
226
|
+
return defaultRuntime.snapshotsAsync(sessions);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function meshSnapshot(projectId: string, sessions?: SessionInfo[]): MeshSnapshot | undefined {
|
|
230
|
+
return defaultRuntime.snapshot(projectId, sessions);
|
|
256
231
|
}
|
|
257
232
|
|
|
258
233
|
export function requestProjectCapability(input: ProjectCapabilityRequestSet): boolean {
|
|
259
234
|
return defaultRuntime.requestCapability(input);
|
|
260
235
|
}
|
|
261
236
|
|
|
262
|
-
export async function meshSnapshotsWithEngine(): Promise<MeshSnapshot[]> {
|
|
263
|
-
return Promise.all(
|
|
237
|
+
export async function meshSnapshotsWithEngine(snapshots = meshSnapshots()): Promise<MeshSnapshot[]> {
|
|
238
|
+
return Promise.all(snapshots.map((snapshot) => enrichMeshSnapshot(snapshot)));
|
|
264
239
|
}
|
|
265
240
|
|
|
266
241
|
export async function meshSnapshotWithEngine(projectId: string): Promise<MeshSnapshot | undefined> {
|
|
@@ -268,52 +243,7 @@ export async function meshSnapshotWithEngine(projectId: string): Promise<MeshSna
|
|
|
268
243
|
return snapshot ? enrichMeshSnapshot(snapshot) : undefined;
|
|
269
244
|
}
|
|
270
245
|
|
|
271
|
-
|
|
272
|
-
type GraphAnalysisDependencies = {
|
|
273
|
-
snapshots: typeof meshSnapshots;
|
|
274
|
-
wait: typeof waitForProjectBindingSync;
|
|
275
|
-
sync?: typeof refreshLocalGraphBindings;
|
|
276
|
-
backbone: typeof projectBackbone;
|
|
277
|
-
graphs: typeof projectRepositoryGraphs;
|
|
278
|
-
send: (client: RelayClient, snapshot: MeshSnapshot) => Promise<void>;
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const graphAnalysisDependencies: GraphAnalysisDependencies = {
|
|
282
|
-
snapshots: meshSnapshots,
|
|
283
|
-
wait: waitForProjectBindingSync,
|
|
284
|
-
sync: refreshLocalGraphBindings,
|
|
285
|
-
backbone: projectBackbone,
|
|
286
|
-
graphs: projectRepositoryGraphs,
|
|
287
|
-
send: (client, snapshot) => sendMeshPayload(client, snapshot, "phone", {
|
|
288
|
-
ttlMs: 60_000, reliable: true,
|
|
289
|
-
}),
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
export async function analyzeProjectGraphNow(
|
|
293
|
-
client: RelayClient, projectId: string,
|
|
294
|
-
dependencies: GraphAnalysisDependencies = graphAnalysisDependencies,
|
|
295
|
-
): Promise<boolean> {
|
|
296
|
-
const snapshot = dependencies.snapshots().find((item) => item.projectId === projectId);
|
|
297
|
-
if (!snapshot) return false;
|
|
298
|
-
await dependencies.wait(projectId);
|
|
299
|
-
const current = dependencies.snapshots().find((item) => item.projectId === projectId);
|
|
300
|
-
if (!current) return false;
|
|
301
|
-
if (dependencies.sync) await dependencies.sync(current);
|
|
302
|
-
const [backbone, repositoryGraphs] = await Promise.all([
|
|
303
|
-
dependencies.backbone(projectId),
|
|
304
|
-
dependencies.graphs(projectId, current.bindings ?? [], { background: false }),
|
|
305
|
-
]);
|
|
306
|
-
const reports = repositoryGraphs.length > 0 ? repositoryGraphs : [{
|
|
307
|
-
projectId, repositoryId: current.project.canonicalRepositoryId,
|
|
308
|
-
revision: "unverified", weavatrixVersion: "unknown",
|
|
309
|
-
analysisStatus: "UNAVAILABLE" as const, analysisErrorCode: "REPOSITORY_NOT_BOUND",
|
|
310
|
-
nodes: [], relations: [], totalNodes: 0, totalRelations: 0, truncated: false,
|
|
311
|
-
}];
|
|
312
|
-
await dependencies.send(client, {
|
|
313
|
-
...current, backbone, repositoryGraphs: reports, generatedAt: Date.now(),
|
|
314
|
-
});
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
246
|
+
export { analyzeProjectGraphNow } from "./graph/analysis";
|
|
317
247
|
|
|
318
248
|
export function handleMeshPayload(
|
|
319
249
|
client: RelayClient,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MeshSnapshot } from "../../../../../../packages/protocol/schema";
|
|
2
|
+
import type { MeshRuntimeDependencies } from "../dependencies";
|
|
3
|
+
import { catalogFromEndpoint } from "../../catalog/models";
|
|
4
|
+
import { projectMcpServers } from "../../catalog/project/capabilities";
|
|
5
|
+
import { configuredProjectMcpServers, mergeConfiguredMcpServers } from "../../catalog/project/configured-mcp";
|
|
6
|
+
import { projectCapabilityObservations } from "../../catalog/project/observations";
|
|
7
|
+
import { projectExecutionCapabilitySessions } from "../capabilities";
|
|
8
|
+
|
|
9
|
+
export function composeMeshSnapshot(
|
|
10
|
+
deps: MeshRuntimeDependencies,
|
|
11
|
+
projectId: string,
|
|
12
|
+
store = deps.store(),
|
|
13
|
+
sessions = deps.sessions(),
|
|
14
|
+
models = catalogFromEndpoint(deps.computer(), sessions),
|
|
15
|
+
): MeshSnapshot | undefined {
|
|
16
|
+
const snapshot = store.snapshot(projectId, deps.computer());
|
|
17
|
+
if (!snapshot) return undefined;
|
|
18
|
+
const linked = projectExecutionCapabilitySessions(
|
|
19
|
+
snapshot, sessions, deps.computer(), deps.capabilityInventory,
|
|
20
|
+
);
|
|
21
|
+
const reported = projectMcpServers(
|
|
22
|
+
linked.filter((session) => session.computerId === deps.computer()), projectId,
|
|
23
|
+
);
|
|
24
|
+
const workspaces = (snapshot.bindings ?? [])
|
|
25
|
+
.filter((binding) => binding.endpointId === deps.computer()
|
|
26
|
+
&& binding.available && binding.localPathHint)
|
|
27
|
+
.map((binding) => binding.localPathHint!);
|
|
28
|
+
const mcpServers = mergeConfiguredMcpServers(
|
|
29
|
+
configuredProjectMcpServers(deps.computer(), workspaces), reported,
|
|
30
|
+
);
|
|
31
|
+
const capabilityObservations = projectCapabilityObservations({
|
|
32
|
+
projectId, endpointId: deps.computer(), bound: workspaces.length > 0,
|
|
33
|
+
requests: snapshot.capabilityRequests ?? [], skills: snapshot.skills ?? [],
|
|
34
|
+
mcpServers, now: deps.now(),
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
...snapshot,
|
|
38
|
+
publisherEndpointId: deps.computer(),
|
|
39
|
+
mcpServers: mcpServers.length > 0 ? mcpServers : undefined,
|
|
40
|
+
capabilityObservations: capabilityObservations.length > 0
|
|
41
|
+
? capabilityObservations : undefined,
|
|
42
|
+
modelCatalog: models.models.length > 0 || models.reason ? [models] : undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -40,7 +40,8 @@ export async function enrichMeshSnapshot(
|
|
|
40
40
|
...(snapshot.supersededKnowledgeRecordIds ?? []),
|
|
41
41
|
...supersededProjectKnowledgeIds(snapshot.projectId, allMemory),
|
|
42
42
|
])].sort().slice(-128);
|
|
43
|
-
const enriched = { ...snapshot, backbone,
|
|
43
|
+
const enriched = { ...snapshot, backbone, ...(repositoryGraphs.length || identityFailures.length
|
|
44
|
+
? { repositoryGraphs: [...repositoryGraphs, ...identityFailures] } : {}),
|
|
44
45
|
repositoryDetails: details,
|
|
45
46
|
knowledge: activeProjectKnowledge(snapshot.projectId, allMemory)
|
|
46
47
|
.sort((a, b) => b.recordedAt - a.recordedAt).slice(0, 16),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Session monitor
|
|
2
|
+
|
|
3
|
+
`index.ts` owns discovery, subscriptions and phone delivery for this computer.
|
|
4
|
+
Chat catalogs publish independently of background Mesh enrichment. The Mesh
|
|
5
|
+
publisher reuses known native identities and yields between Projects.
|
|
6
|
+
|
|
7
|
+
Architecture responses acknowledge their explicit request. Coalesced builds
|
|
8
|
+
retain every controller's receipt; periodic cached reports remain ordinary Mesh
|
|
9
|
+
snapshots. Repository analysis uses its own Engine socket to keep policy and
|
|
10
|
+
Backbone requests responsive.
|
|
11
|
+
|
|
12
|
+
Integration tests remain in `tests/monitor`; background publication tests live
|
|
13
|
+
beside `mesh-publication`. This module uses the repository MIT License.
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { RelayClient } from "../../../../packages/core/relay-client";
|
|
2
2
|
import { analyzeProjectGraphNow } from "../mesh/runtime";
|
|
3
3
|
|
|
4
|
-
const inFlight = new Set<string
|
|
4
|
+
const inFlight = new Map<string, Set<string>>();
|
|
5
5
|
|
|
6
6
|
/** One user request starts one analysis per Project, even during reconnect. */
|
|
7
7
|
export function requestProjectGraphRefresh(
|
|
8
8
|
client: RelayClient, projectId: string,
|
|
9
9
|
analyze: typeof analyzeProjectGraphNow = analyzeProjectGraphNow,
|
|
10
|
+
requestId?: string,
|
|
10
11
|
): boolean {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const pending = inFlight.get(projectId);
|
|
13
|
+
if (pending) { if (requestId) pending.add(requestId); return false; }
|
|
14
|
+
const receipts = new Set(requestId ? [requestId] : []);
|
|
15
|
+
inFlight.set(projectId, receipts);
|
|
16
|
+
void analyze(client, projectId, undefined, receipts).catch(() => false).finally(() => {
|
|
14
17
|
inFlight.delete(projectId);
|
|
15
18
|
});
|
|
16
19
|
return true;
|
|
@@ -3,6 +3,7 @@ import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
|
3
3
|
import { sendSessionPayload } from "../../host/session-keys";
|
|
4
4
|
import { cachedSessionActivity } from "../support/session-activity";
|
|
5
5
|
import { readTranscriptHistory } from "../../transcript-history";
|
|
6
|
+
import { publishedSession } from "../../session-catalog";
|
|
6
7
|
import { activityWithImages } from '../../session-images';
|
|
7
8
|
import {
|
|
8
9
|
scanSessionHistory,
|
|
@@ -38,11 +39,13 @@ export function boundedCatalogHistory(
|
|
|
38
39
|
return [...pinned, ...rest].sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function resolveSession(sessionId: string, status?: SessionsStatus): SessionInfo | undefined {
|
|
42
|
+
function resolveSession(client: RelayClient, sessionId: string, status?: SessionsStatus): SessionInfo | undefined {
|
|
42
43
|
if (status) {
|
|
43
44
|
return status.sessions.find((session) => session.sessionId === sessionId)
|
|
44
45
|
?? status.history?.find((session) => session.sessionId === sessionId);
|
|
45
46
|
}
|
|
47
|
+
const known = publishedSession(client, sessionId);
|
|
48
|
+
if (known) return known;
|
|
46
49
|
return scanSessions().sessions.find((session) => session.sessionId === sessionId)
|
|
47
50
|
?? scanSessionHistory().find((session) => session.sessionId === sessionId);
|
|
48
51
|
}
|
|
@@ -55,7 +58,7 @@ export async function publishSessionEvents(
|
|
|
55
58
|
threadId?: string,
|
|
56
59
|
history?: { history?: boolean; historyCursor?: string },
|
|
57
60
|
): Promise<boolean> {
|
|
58
|
-
const session = resolveSession(sessionId, status);
|
|
61
|
+
const session = resolveSession(client, sessionId, status);
|
|
59
62
|
if (!session) return false;
|
|
60
63
|
const activity = threadId ? scanThreadActivity(session, threadId)
|
|
61
64
|
: (history?.history ? readTranscriptHistory(session, history.historyCursor) : undefined)
|
|
@@ -172,10 +172,10 @@ async function handleInboundRefresh(
|
|
|
172
172
|
// An explicit refresh may pay for the full history scan; routine Mesh
|
|
173
173
|
// snapshots use the cached history and fresh live sessions.
|
|
174
174
|
invalidateMeshSessionHistory();
|
|
175
|
-
await publish(true).catch(() => {});
|
|
176
175
|
if (payload.graphProjectId && loadRuntimeConfig().meshEnabled) {
|
|
177
|
-
requestProjectGraphRefresh(client, payload.graphProjectId);
|
|
176
|
+
requestProjectGraphRefresh(client, payload.graphProjectId, undefined, payload.graphAnalysisRequestId);
|
|
178
177
|
}
|
|
178
|
+
void publish(true).catch(() => {});
|
|
179
179
|
return true;
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Mesh publication
|
|
2
|
+
|
|
3
|
+
`index.ts` owns one background Mesh publication per monitor transport. Project
|
|
4
|
+
projection yields to live decisions and uses the observed native catalog. Plain
|
|
5
|
+
snapshots, policy reports, and Engine enrichment remain separate from fresh chat
|
|
6
|
+
catalog delivery. Monitor close stops publication from the old transport.
|
|
7
|
+
|
|
8
|
+
Tests in `tests/` exercise current catalog progress while Mesh delivery is held.
|
|
9
|
+
This module uses the repository MIT License.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
2
|
+
import type { SessionsStatus } from "../../../../../packages/protocol/schema";
|
|
3
|
+
import { localMeshStore } from "../../mesh/local-remote/local";
|
|
4
|
+
import { meshSnapshotsAsync, meshSnapshotsWithEngine } from "../../mesh/runtime";
|
|
5
|
+
import { deriveObservedClaims } from "../../mesh/observed/claims";
|
|
6
|
+
import { ingestRuntimeInvocations } from "../../engine/invocation/ingest";
|
|
7
|
+
import { publishProjectPolicyStatuses } from "../../project-policy/runtime";
|
|
8
|
+
import { publishedCatalog } from "../../session-catalog";
|
|
9
|
+
import { sendMeshPayload } from "../../host/session-keys";
|
|
10
|
+
import { INTERVAL_MS } from "../handlers/catalog";
|
|
11
|
+
|
|
12
|
+
const pending = new WeakMap<RelayClient, { active: boolean }>();
|
|
13
|
+
|
|
14
|
+
/** One background projection per transport; it never holds the current chat catalog. */
|
|
15
|
+
export function publishMonitorMeshes(client: RelayClient, status: SessionsStatus): void {
|
|
16
|
+
if (pending.has(client)) return;
|
|
17
|
+
const token = { active: true };
|
|
18
|
+
pending.set(client, token);
|
|
19
|
+
const work = (async () => {
|
|
20
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
21
|
+
if (!token.active || !client.isConnected) return;
|
|
22
|
+
deriveObservedClaims(localMeshStore(), status.sessions);
|
|
23
|
+
void ingestRuntimeInvocations(status.sessions).catch(() => {});
|
|
24
|
+
const known = publishedCatalog(client);
|
|
25
|
+
const snapshots = await meshSnapshotsAsync([...status.sessions, ...(known?.history ?? [])]);
|
|
26
|
+
for (const snapshot of snapshots) {
|
|
27
|
+
if (!token.active || !client.isConnected) return;
|
|
28
|
+
await sendMeshPayload(client, snapshot, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false }).catch(() => {});
|
|
29
|
+
}
|
|
30
|
+
if (!token.active || !client.isConnected) return;
|
|
31
|
+
await publishProjectPolicyStatuses(client, snapshots.map(mesh => mesh.projectId)).catch(() => {});
|
|
32
|
+
const enriched = await meshSnapshotsWithEngine(snapshots);
|
|
33
|
+
for (const snapshot of enriched) {
|
|
34
|
+
if (!token.active || !client.isConnected) return;
|
|
35
|
+
await sendMeshPayload(client, snapshot, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false }).catch(() => {});
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
void work.catch(() => {}).finally(() => { if (pending.get(client) === token) pending.delete(client); });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function stopMonitorMeshes(client: RelayClient): void {
|
|
42
|
+
const token = pending.get(client);
|
|
43
|
+
if (token) token.active = false;
|
|
44
|
+
pending.delete(client);
|
|
45
|
+
}
|
|
@@ -12,25 +12,22 @@ import { localMeshStore } from "../../mesh/local-remote/local";
|
|
|
12
12
|
import { refreshMcpLoad } from "../../machine-load/mcp/refresh";
|
|
13
13
|
import { approvalsStatus } from "../../approvals/state";
|
|
14
14
|
import { primeSessionKeys } from "../../host/session-keys";
|
|
15
|
-
import {
|
|
16
|
-
import { meshCatalog, meshSnapshots, meshSnapshotsWithEngine } from "../../mesh/runtime";
|
|
17
|
-
import { deriveObservedClaims } from "../../mesh/observed/claims";
|
|
18
|
-
import { ingestRuntimeInvocations } from "../../engine/invocation/ingest";
|
|
15
|
+
import { meshCatalog } from "../../mesh/runtime";
|
|
19
16
|
import { HEARTBEAT_INTERVAL_MS, publishHeartbeat } from "./heartbeat";
|
|
20
17
|
import { confirmPendingController } from "../../pairing/controllers";
|
|
21
18
|
import { recordControllerName } from "../../pairing/presence";
|
|
22
19
|
import { startPublishLoop } from "./publish-loop";
|
|
23
20
|
import { singleFlightPublisher } from "./single-flight";
|
|
21
|
+
import { publishedCatalog, rememberPublishedCatalog, resumePublishedChats } from "../../session-catalog";
|
|
24
22
|
import { createMachineLoadPublisher } from "../../machine-load";
|
|
25
23
|
import { startMachineLoadLoop } from "../../machine-load/host/loop";
|
|
26
|
-
import { publishProjectPolicyStatuses } from "../../project-policy/runtime";
|
|
27
24
|
import { scanCapabilityUsage, scanSessionHistory, scanSessions, scopeCapabilityUsageToRoom, TOKEN_WINDOW_HOURS } from "../../sessions";
|
|
28
25
|
import { boundedCatalogHistory, INTERVAL_MS, publishSessionEvents } from "../handlers/catalog";
|
|
29
26
|
import { sweepAttachments } from "../handlers/receipts";
|
|
30
27
|
import { handleMonitorMessage } from "../handlers/inbound";
|
|
31
28
|
|
|
32
29
|
const HISTORY_INTERVAL_MS = 10_000;
|
|
33
|
-
|
|
30
|
+
import { publishMonitorMeshes, stopMonitorMeshes } from "../mesh-publication";
|
|
34
31
|
|
|
35
32
|
export type SessionMonitor = {
|
|
36
33
|
publish: () => Promise<void>;
|
|
@@ -121,8 +118,13 @@ async function publishMonitorTick(input: {
|
|
|
121
118
|
if (!client.isConnected) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
|
|
122
119
|
await publishHeartbeat(client).catch(() => {});
|
|
123
120
|
if (!leadership.acquire()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
|
|
121
|
+
if (lastHistoryPublishedAt === 0) {
|
|
122
|
+
await resumePublishedChats(client,
|
|
123
|
+
(session, status) => publishSessionEvents(client, session.sessionId, status));
|
|
124
|
+
}
|
|
124
125
|
const includeHistory = forceHistory || Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS;
|
|
125
126
|
const status = monitorSnapshot({ includeHistory, forceHistory, subscriptions, history });
|
|
127
|
+
rememberPublishedCatalog(client, status);
|
|
126
128
|
await client.send(status, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
|
|
127
129
|
// Provider review is independent of task visibility; a hung hook RPC must
|
|
128
130
|
// never hold the current catalog behind its discovery timeout.
|
|
@@ -134,22 +136,7 @@ async function publishMonitorTick(input: {
|
|
|
134
136
|
await publishSessionEvents(client, sessionId, status).catch(() => false);
|
|
135
137
|
}
|
|
136
138
|
await client.send(approvalsStatus(), "phone", { ttlMs: INTERVAL_MS * 3, reliable: false });
|
|
137
|
-
if (loadRuntimeConfig().meshEnabled)
|
|
138
|
-
deriveObservedClaims(localMeshStore(), status.sessions);
|
|
139
|
-
void ingestRuntimeInvocations(status.sessions).catch(() => {});
|
|
140
|
-
const meshes = meshSnapshots();
|
|
141
|
-
for (const mesh of meshes) {
|
|
142
|
-
await sendMeshPayload(client, mesh, "phone", {
|
|
143
|
-
ttlMs: INTERVAL_MS * 24,
|
|
144
|
-
reliable: false,
|
|
145
|
-
}).catch(() => {});
|
|
146
|
-
}
|
|
147
|
-
await publishProjectPolicyStatuses(
|
|
148
|
-
client,
|
|
149
|
-
meshes.map((mesh) => mesh.projectId),
|
|
150
|
-
).catch(() => {});
|
|
151
|
-
publishEnrichedMeshes(client);
|
|
152
|
-
}
|
|
139
|
+
if (loadRuntimeConfig().meshEnabled) publishMonitorMeshes(client, status);
|
|
153
140
|
if (Date.now() - lastCapabilityPublishedAt >= 30_000) {
|
|
154
141
|
const usage = scanCapabilityUsage([
|
|
155
142
|
...status.sessions,
|
|
@@ -161,18 +148,6 @@ async function publishMonitorTick(input: {
|
|
|
161
148
|
return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
|
|
162
149
|
}
|
|
163
150
|
|
|
164
|
-
function publishEnrichedMeshes(client: RelayClient): void {
|
|
165
|
-
if (meshEnrichmentInFlight) return;
|
|
166
|
-
meshEnrichmentInFlight = (async () => {
|
|
167
|
-
const meshes = await meshSnapshotsWithEngine();
|
|
168
|
-
for (const mesh of meshes) {
|
|
169
|
-
await sendMeshPayload(client, mesh, "phone", {
|
|
170
|
-
ttlMs: INTERVAL_MS * 24, reliable: false,
|
|
171
|
-
}).catch(() => {});
|
|
172
|
-
}
|
|
173
|
-
})().finally(() => { meshEnrichmentInFlight = undefined; });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
151
|
/**
|
|
177
152
|
* Attach task discovery and phone-to-agent delivery to the same relay client
|
|
178
153
|
* used by MCP tools. This keeps STDIO stdout clean and removes the old need for
|
|
@@ -182,6 +157,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
|
|
|
182
157
|
const subscriptions = new Set<string>();
|
|
183
158
|
const leadership = monitorLeadership();
|
|
184
159
|
primeSessionKeys(client);
|
|
160
|
+
publishedCatalog(client);
|
|
185
161
|
let historyCache: { generatedAt: number; sessions: SessionsStatus["sessions"] } | undefined;
|
|
186
162
|
let lastHistoryPublishedAt = 0;
|
|
187
163
|
let lastCapabilityPublishedAt = 0;
|
|
@@ -247,6 +223,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
|
|
|
247
223
|
publish: () => publish(false),
|
|
248
224
|
close: () => {
|
|
249
225
|
stopCatalog();
|
|
226
|
+
stopMonitorMeshes(client);
|
|
250
227
|
stopHeartbeat();
|
|
251
228
|
loadLoop.stop();
|
|
252
229
|
off();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Session catalog
|
|
2
|
+
|
|
3
|
+
`index.ts` keeps the native identities of the latest published catalog. Requests
|
|
4
|
+
for a chat already shown on the phone use that identity instead of walking every
|
|
5
|
+
provider directory again, including image chunk requests. The in-memory catalog
|
|
6
|
+
retains current names; the private restart cache contains only native identity. A new discovery window cannot make a known chat
|
|
7
|
+
unreadable merely because hundreds of newer logs appeared.
|
|
8
|
+
|
|
9
|
+
The private `session-catalog-cache.json` contains metadata and validated native
|
|
10
|
+
source locations. It excludes messages, capability descriptions, and credentials.
|
|
11
|
+
The room and provider roots bind its scope. On restart the primary monitor sends
|
|
12
|
+
the saved Now chats' current native heads under their independent task keys before
|
|
13
|
+
performing an expensive fresh discovery scan. It does not republish an old catalog
|
|
14
|
+
as fresh. The phone continues its durable historical backfill independently.
|
|
15
|
+
|
|
16
|
+
Tests live in `tests/`. This runtime module uses the repository MIT License.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { RelayClient } from "../../../../packages/core/relay-client";
|
|
5
|
+
import { SessionsStatus, type SessionInfo } from "../../../../packages/protocol/schema";
|
|
6
|
+
import { configDir, writePrivateFile } from "../config";
|
|
7
|
+
import { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "../sessions";
|
|
8
|
+
|
|
9
|
+
const catalogs = new WeakMap<RelayClient, SessionsStatus | undefined>();
|
|
10
|
+
const cachePath = () => join(configDir(), "session-catalog-cache.json");
|
|
11
|
+
const scope = (client: RelayClient) => createHash("sha256")
|
|
12
|
+
.update([client.room, ...Object.values(nativeTranscriptRoots())].join("\0")).digest("hex");
|
|
13
|
+
|
|
14
|
+
function metadata(session: SessionInfo): SessionInfo {
|
|
15
|
+
const { sessionId, agent, state, startedAt, lastActivityAt, tokensSession, tokensLastTurn,
|
|
16
|
+
cwd, model, projectId, taskId, computerId } = session;
|
|
17
|
+
return { sessionId, agent, state, startedAt, lastActivityAt, tokensSession, tokensLastTurn,
|
|
18
|
+
cwd, model, projectId, taskId, computerId };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Keep native identity, not capability descriptors or message contents. */
|
|
22
|
+
export function rememberPublishedCatalog(client: RelayClient, status: SessionsStatus): void {
|
|
23
|
+
const previous = catalogs.get(client);
|
|
24
|
+
const history = status.history ?? previous?.history;
|
|
25
|
+
const compact: SessionsStatus = { type: "sessions.status", machine: status.machine,
|
|
26
|
+
sessions: status.sessions.map(metadata), history: history?.map(metadata),
|
|
27
|
+
generatedAt: status.generatedAt };
|
|
28
|
+
catalogs.set(client, { ...status, history });
|
|
29
|
+
const paths = Object.fromEntries([...compact.sessions, ...(compact.history ?? [])]
|
|
30
|
+
.flatMap(session => { const path = nativeTranscriptSource(session);
|
|
31
|
+
return path ? [[session.sessionId, path]] : []; }));
|
|
32
|
+
try { writePrivateFile(cachePath(), JSON.stringify({ scope: scope(client), status: compact, paths })); }
|
|
33
|
+
catch { /* The current in-memory catalog remains readable on a full disk. */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function publishedCatalog(client: RelayClient): SessionsStatus | undefined {
|
|
37
|
+
if (catalogs.has(client)) return catalogs.get(client);
|
|
38
|
+
let catalog: SessionsStatus | undefined;
|
|
39
|
+
try {
|
|
40
|
+
if (statSync(cachePath()).size > 1024 * 1024) throw new Error("catalog cache exceeds limit");
|
|
41
|
+
const record = JSON.parse(readFileSync(cachePath(), "utf8"));
|
|
42
|
+
const parsed = SessionsStatus.safeParse(record.status);
|
|
43
|
+
if (record.scope === scope(client) && parsed.success) {
|
|
44
|
+
catalog = parsed.data;
|
|
45
|
+
for (const session of [...catalog.sessions, ...(catalog.history ?? [])]) {
|
|
46
|
+
const path = record.paths?.[session.sessionId];
|
|
47
|
+
if (typeof path === "string") restoreNativeTranscriptSource(session, path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch { /* Missing, malformed, or foreign catalogs fall back to discovery. */ }
|
|
51
|
+
catalogs.set(client, catalog);
|
|
52
|
+
return catalog;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function publishedSession(client: RelayClient, sessionId: string): SessionInfo | undefined {
|
|
56
|
+
const status = publishedCatalog(client);
|
|
57
|
+
return status?.sessions.find(session => session.sessionId === sessionId)
|
|
58
|
+
?? status?.history?.find(session => session.sessionId === sessionId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Send saved Now heads under their task keys before an expensive fresh scan. */
|
|
62
|
+
export async function resumePublishedChats(client: RelayClient,
|
|
63
|
+
publish: (session: SessionInfo, status: SessionsStatus) => Promise<unknown>): Promise<void> {
|
|
64
|
+
const status = publishedCatalog(client);
|
|
65
|
+
if (!status) return;
|
|
66
|
+
for (const session of status.sessions) {
|
|
67
|
+
if (session.agent !== "codex" && session.agent !== "claude") continue;
|
|
68
|
+
try { await publish(session, status); } catch { /* Retry on the regular tick. */ }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -12,3 +12,8 @@ account state. Apple clients retain validated original bytes locally and use
|
|
|
12
12
|
their scoped copy when offline. Tests live in `tests/`.
|
|
13
13
|
|
|
14
14
|
Licensed under the MIT License. See the repository root `LICENSE`.
|
|
15
|
+
|
|
16
|
+
Native image identities remain readable after the chat head advances. Codex
|
|
17
|
+
attachments are located by timestamp and content block together, so a caption
|
|
18
|
+
with the same timestamp cannot hide the picture. Original bytes are delivered in
|
|
19
|
+
bounded encrypted chunks and the Apple client retains them for offline use.
|
|
@@ -3,6 +3,7 @@ import { SessionImageChunk } from '../../../../packages/protocol/schema';
|
|
|
3
3
|
import type { RelayClient } from '../../../../packages/core/relay-client';
|
|
4
4
|
import { sendSessionPayload } from '../host/session-keys';
|
|
5
5
|
import { readTranscriptHistory } from '../transcript-history';
|
|
6
|
+
import { publishedSession } from '../session-catalog';
|
|
6
7
|
import { cachedSessionActivity } from '../monitor/support/session-activity';
|
|
7
8
|
import { scanSessions, scanSessionHistory } from '../sessions';
|
|
8
9
|
import { sessionImageChunk, transcriptImages } from '../../../mcp/src/desktop/image';
|
|
@@ -22,7 +23,8 @@ export function readSessionImage(query: SessionImageQuery, session: SessionInfo,
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export async function publishSessionImage(client: RelayClient, query: SessionImageQuery): Promise<boolean> {
|
|
25
|
-
const session =
|
|
26
|
+
const session = publishedSession(client, query.sessionId)
|
|
27
|
+
?? scanSessions().sessions.find(s => s.sessionId === query.sessionId)
|
|
26
28
|
?? scanSessionHistory().find(s => s.sessionId === query.sessionId);
|
|
27
29
|
const activity = session ? readTranscriptHistory(session, query.historyCursor)
|
|
28
30
|
?? cachedSessionActivity(session) : undefined;
|
|
@@ -50,6 +50,7 @@ import { loadRuntimeConfig } from "../config";
|
|
|
50
50
|
import { timedProviderScan } from "../machine-load/mcp/scan-cost";
|
|
51
51
|
|
|
52
52
|
export { MAX_ACTIVITY_ENTRIES, MAX_ACTIVITY_TEXT, TOKEN_WINDOW_HOURS };
|
|
53
|
+
export { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "./native-source";
|
|
53
54
|
|
|
54
55
|
type ProviderScan = ReturnType<typeof scanClaude>;
|
|
55
56
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, isAbsolute, relative } from "node:path";
|
|
3
|
+
import type { SessionInfo } from "../../../../packages/protocol/schema";
|
|
4
|
+
import { claudeLogPathBySession } from "./scan/claude/shared";
|
|
5
|
+
import { codexLogPathBySession } from "./scan/codex/shared";
|
|
6
|
+
import { claudeProjectsRoot, codexSessionsRoot } from "./support/common";
|
|
7
|
+
|
|
8
|
+
export function nativeTranscriptRoots(): { claude: string; codex: string } {
|
|
9
|
+
return { claude: claudeProjectsRoot(), codex: codexSessionsRoot() };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Native paths come only from discovery; a phone never supplies one. */
|
|
13
|
+
export function nativeTranscriptSource(session: SessionInfo): string | undefined {
|
|
14
|
+
if (session.agent === "claude") return claudeLogPathBySession.get(session.sessionId);
|
|
15
|
+
if (session.agent === "codex") return codexLogPathBySession.get(session.sessionId);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Restore an owned native source without walking every provider's logs. */
|
|
20
|
+
export function restoreNativeTranscriptSource(session: SessionInfo, path: string): boolean {
|
|
21
|
+
if (!["claude", "codex"].includes(session.agent)
|
|
22
|
+
|| !/^[a-zA-Z0-9-]{1,128}$/.test(session.sessionId)) return false;
|
|
23
|
+
const name = basename(path);
|
|
24
|
+
if (session.agent === "claude" ? name !== `${session.sessionId}.jsonl`
|
|
25
|
+
: !name.startsWith("rollout-") || !name.endsWith(`-${session.sessionId}.jsonl`)) return false;
|
|
26
|
+
try {
|
|
27
|
+
if (!lstatSync(path).isFile()) return false;
|
|
28
|
+
const root = session.agent === "claude" ? claudeProjectsRoot() : codexSessionsRoot();
|
|
29
|
+
const inside = relative(realpathSync(root), realpathSync(path));
|
|
30
|
+
if (!inside || inside.startsWith("..") || isAbsolute(inside)) return false;
|
|
31
|
+
const index = session.agent === "claude" ? claudeLogPathBySession : codexLogPathBySession;
|
|
32
|
+
index.set(session.sessionId, path);
|
|
33
|
+
return true;
|
|
34
|
+
} catch { return false; }
|
|
35
|
+
}
|
|
@@ -21,14 +21,16 @@ export function nativeTranscriptImage(session: SessionInfo, imageId: string, off
|
|
|
21
21
|
}
|
|
22
22
|
if (!path || !['claude', 'codex'].includes(session.agent)) return undefined;
|
|
23
23
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
let pageCursor = cursor;
|
|
25
|
+
for (let page = 0; page < 32; page++) {
|
|
26
|
+
const window = previousNativeLines(path, session.sessionId, pageCursor);
|
|
27
|
+
if (!window) return undefined;
|
|
28
|
+
const lines = window.lines.map(line => line.text);
|
|
29
|
+
if (session.agent === 'codex') {
|
|
30
|
+
const full = codexImageChunk(session.sessionId, imageId, 0, lines)
|
|
31
|
+
?? codexImageChunk(session.sessionId, imageId, 0);
|
|
32
|
+
return full ? codexImageChunk(session.sessionId, imageId, offset) : undefined;
|
|
33
|
+
}
|
|
32
34
|
for (const line of lines) {
|
|
33
35
|
let row;
|
|
34
36
|
try { row = JSON.parse(line); } catch { continue; }
|
|
@@ -46,6 +48,9 @@ export function nativeTranscriptImage(session: SessionInfo, imageId: string, off
|
|
|
46
48
|
return slice(cached, offset);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
if (window.start === 0) break;
|
|
52
|
+
pageCursor = window.cursorAt(window.lines[0]?.offset ?? window.start);
|
|
53
|
+
}
|
|
49
54
|
} catch { return undefined; }
|
|
50
55
|
return undefined;
|
|
51
56
|
}
|
|
@@ -5,9 +5,10 @@ import { previousNativeLines } from "./native-lines";
|
|
|
5
5
|
/** Locate an older image row without copying image bytes into chat pages. */
|
|
6
6
|
export function nativeImageLines(sessionId: string, entryId: string): string[] | undefined {
|
|
7
7
|
if (!entryId.startsWith(`${sessionId}:`)) return undefined;
|
|
8
|
-
const
|
|
8
|
+
const [stampText, blockText] = entryId.slice(sessionId.length + 1).split(":");
|
|
9
|
+
const stamp = Number(stampText), blockIndex = Number(blockText);
|
|
9
10
|
const path = codexLogPathBySession.get(sessionId);
|
|
10
|
-
if (!path || !Number.isFinite(stamp)) return undefined;
|
|
11
|
+
if (!path || !Number.isFinite(stamp) || !Number.isSafeInteger(blockIndex) || blockIndex < 0) return undefined;
|
|
11
12
|
let cursor: string | undefined;
|
|
12
13
|
try {
|
|
13
14
|
for (let page = 0; page < 32; page++) {
|
|
@@ -19,7 +20,8 @@ export function nativeImageLines(sessionId: string, entryId: string): string[] |
|
|
|
19
20
|
const createdAt = ts(row?.timestamp);
|
|
20
21
|
if (createdAt) earliest = Math.min(earliest, createdAt);
|
|
21
22
|
if (createdAt === stamp && row?.type === "response_item" && p?.type === "message"
|
|
22
|
-
&& p.role === "user" && Array.isArray(p.content)
|
|
23
|
+
&& p.role === "user" && Array.isArray(p.content)
|
|
24
|
+
&& p.content[blockIndex]?.type === "input_image") return [line.text];
|
|
23
25
|
}
|
|
24
26
|
if (window.start === 0 || earliest < stamp) return undefined;
|
|
25
27
|
cursor = window.cursorAt(window.lines[0]?.offset ?? window.start);
|
|
@@ -15,7 +15,10 @@ export function sessionImageChunk(session: SessionInfo, activity: SessionActivit
|
|
|
15
15
|
if (activity.sessionId !== session.sessionId) return undefined;
|
|
16
16
|
const native = activity.entries.some(entry => entry.id === imageId && entry.kind === 'user'
|
|
17
17
|
&& entry.attachments?.includes('Image'));
|
|
18
|
-
if (native
|
|
18
|
+
if (native || imageId.startsWith(`${session.sessionId}:`)) {
|
|
19
|
+
const picture = nativeTranscriptImage(session, imageId, offset, cursor);
|
|
20
|
+
if (picture) return picture;
|
|
21
|
+
}
|
|
19
22
|
const artifact = activity.entries.flatMap(artifactImages).find(image => image.id === imageId);
|
|
20
23
|
if (!artifact) return undefined;
|
|
21
24
|
const loaded = readStoreState(storePath ?? join(configDir(), 'project-mesh.json'));
|
package/cursor-plugin/README.md
CHANGED
|
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
|
|
|
10
10
|
Install the supported CLI on this computer, then run:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g granttap-mcp@0.8.
|
|
13
|
+
npm install -g granttap-mcp@0.8.48
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "node",
|
|
5
5
|
"args": [
|
|
6
6
|
"-e",
|
|
7
|
-
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.
|
|
7
|
+
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.48\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
|
|
8
8
|
]
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
|
|
|
7
7
|
const { existsSync } = require("node:fs");
|
|
8
8
|
const { delimiter, dirname, join } = require("node:path");
|
|
9
9
|
|
|
10
|
-
const PACKAGE = "granttap-mcp@0.8.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.48";
|
|
11
11
|
const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
|
|
12
12
|
const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
|
|
13
13
|
? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
|
package/package.json
CHANGED
|
@@ -144,5 +144,6 @@ export const SessionsRefresh = z.object({
|
|
|
144
144
|
type: z.literal("sessions.refresh"),
|
|
145
145
|
createdAt: z.number(),
|
|
146
146
|
graphProjectId: z.string().min(1).max(128).optional(),
|
|
147
|
+
graphAnalysisRequestId: z.string().min(1).max(128).optional(),
|
|
147
148
|
});
|
|
148
149
|
export type SessionsRefresh = z.infer<typeof SessionsRefresh>;
|
|
@@ -250,6 +250,7 @@ export const MeshSnapshot = z.object({
|
|
|
250
250
|
dependencies: z.array(TaskDependency).max(128),
|
|
251
251
|
events: z.array(MeshEvent).max(128),
|
|
252
252
|
generatedAt: z.number().nonnegative(),
|
|
253
|
+
graphAnalysisRequestId: z.string().min(1).max(128).optional(),
|
|
253
254
|
}).strict().superRefine((value, ctx) => {
|
|
254
255
|
const detailKeys = new Set(value.repositoryDetails?.map((item) => `${item.endpointId}\0${item.repositoryId}`));
|
|
255
256
|
if (detailKeys.size !== (value.repositoryDetails?.length ?? 0)
|