granttap-mcp 0.8.95 → 0.8.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/apps/bridge/src/delivery/index.ts +20 -6
  2. package/apps/bridge/src/engine/README.md +7 -0
  3. package/apps/bridge/src/engine/distribution/artifacts.ts +2 -2
  4. package/apps/bridge/src/engine/protocol/project-relationships/README.md +12 -0
  5. package/apps/bridge/src/engine/protocol/project-relationships/index.ts +41 -0
  6. package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +3 -0
  7. package/apps/bridge/src/engine/protocol/transport/result-parser.ts +2 -0
  8. package/apps/bridge/src/engine/runtime/engine-client.ts +1 -0
  9. package/apps/bridge/src/engine/runtime/project-relationships/README.md +19 -0
  10. package/apps/bridge/src/engine/runtime/project-relationships/index.ts +71 -0
  11. package/apps/bridge/src/engine/runtime/repository-graph-jobs.ts +6 -0
  12. package/apps/bridge/src/mesh/admin/convergence.ts +7 -1
  13. package/apps/bridge/src/mesh/catalog/index.ts +2 -0
  14. package/apps/bridge/src/mesh/runtime/execution-sweep.ts +2 -0
  15. package/apps/bridge/src/mesh/runtime/graph/README.md +7 -0
  16. package/apps/bridge/src/mesh/runtime/graph/analysis.ts +7 -1
  17. package/apps/bridge/src/mesh/runtime/snapshot/enriched.ts +6 -1
  18. package/apps/bridge/src/mesh/snapshot/merge.ts +3 -2
  19. package/apps/bridge/src/mesh/store/index.ts +2 -2
  20. package/apps/bridge/src/mesh/store/receipts/execution-merge.ts +10 -0
  21. package/apps/bridge/src/mesh/store/receipts/prepared-availability.ts +55 -0
  22. package/apps/bridge/src/mesh/store/sync.ts +4 -3
  23. package/apps/bridge/src/monitor/handlers/inbound.ts +3 -0
  24. package/apps/bridge/src/monitor/handlers/receipts.ts +3 -1
  25. package/apps/bridge/src/monitor/handlers/user-message.ts +1 -1
  26. package/apps/bridge/src/monitor/message-delivery/README.md +6 -0
  27. package/apps/bridge/src/monitor/message-delivery/index.ts +2 -1
  28. package/apps/bridge/src/monitor/message-delivery/status-query.ts +12 -0
  29. package/apps/bridge/src/providers/grok-activity/README.md +13 -0
  30. package/apps/bridge/src/providers/grok-activity/index.ts +64 -0
  31. package/apps/bridge/src/reply/process/provider-headless.ts +5 -0
  32. package/apps/bridge/src/sessions/scan/grok.ts +3 -1
  33. package/apps/bridge/src/task-transfer/README.md +7 -0
  34. package/apps/bridge/src/task-transfer/index.ts +7 -0
  35. package/cursor-plugin/README.md +1 -1
  36. package/cursor-plugin/mcp.json +1 -1
  37. package/cursor-plugin/stdio-bootstrap.cjs +1 -1
  38. package/package.json +1 -1
  39. package/packages/protocol/messages/interaction/index.ts +7 -0
  40. package/packages/protocol/schema.ts +2 -0
@@ -13,9 +13,12 @@ type DeliveryRecord = {
13
13
  updatedAt: number;
14
14
  processingAcknowledged?: boolean;
15
15
  rejectedError?: string;
16
+ scope?: DeliveryScope;
16
17
  };
17
18
 
18
- export type DeliveryStart = "started" | "processing" | "completed";
19
+ export type DeliveryScope = { room: string; peerPublicKey: string; sessionId: string };
20
+
21
+ export type DeliveryStart = "started" | "processing" | "completed" | "foreign";
19
22
 
20
23
  export function deliveryLedgerPath(): string {
21
24
  return join(configDir(), "delivery-ledger.json");
@@ -62,7 +65,7 @@ function save(records: Record<string, DeliveryRecord>, now: number): void {
62
65
  export function acknowledgeProcessingDelivery(messageId: string, now = Date.now()): void {
63
66
  const records = load();
64
67
  if (records[messageId]?.state !== "processing") return;
65
- records[messageId] = { state: "processing", updatedAt: now, processingAcknowledged: true };
68
+ records[messageId] = { ...records[messageId], state: "processing", updatedAt: now, processingAcknowledged: true };
66
69
  save(records, now);
67
70
  }
68
71
 
@@ -72,28 +75,30 @@ export function processingDeliveryAcknowledged(messageId: string): boolean {
72
75
  }
73
76
 
74
77
  /** Acquire a durable processing lease before invoking an agent. */
75
- export function beginDelivery(messageId: string, now = Date.now()): DeliveryStart {
78
+ export function beginDelivery(messageId: string, now = Date.now(), scope?: DeliveryScope): DeliveryStart {
76
79
  const records = load();
77
80
  const current = records[messageId];
81
+ if (current?.scope && (!scope || current.scope.room !== scope.room
82
+ || current.scope.peerPublicKey !== scope.peerPublicKey || current.scope.sessionId !== scope.sessionId)) return 'foreign';
78
83
  if (current?.state === "completed" && current.updatedAt >= now - MAX_AGE_MS) return "completed";
79
84
  if (current?.state === "processing" && current.updatedAt >= now - PROCESSING_LEASE_MS) {
80
85
  return "processing";
81
86
  }
82
- records[messageId] = { state: "processing", updatedAt: now };
87
+ records[messageId] = { state: "processing", updatedAt: now, scope };
83
88
  save(records, now);
84
89
  return "started";
85
90
  }
86
91
 
87
92
  export function completeDelivery(messageId: string, now = Date.now()): void {
88
93
  const records = load();
89
- records[messageId] = { state: "completed", updatedAt: now };
94
+ records[messageId] = { ...records[messageId], state: "completed", updatedAt: now };
90
95
  save(records, now);
91
96
  }
92
97
 
93
98
  /** A late provider refusal remains terminal across reconnects and replayed boxes. */
94
99
  export function rejectCompletedDelivery(messageId: string, error: string, now = Date.now()): void {
95
100
  const records = load();
96
- records[messageId] = { state: 'completed', updatedAt: now, rejectedError: error.slice(0, 1_000) };
101
+ records[messageId] = { ...records[messageId], state: 'completed', updatedAt: now, rejectedError: error.slice(0, 500) };
97
102
  save(records, now);
98
103
  }
99
104
 
@@ -103,6 +108,15 @@ export function completedDeliveryFailure(messageId: string): string | undefined
103
108
  ? record.rejectedError : undefined;
104
109
  }
105
110
 
111
+ /** Receipt replay is scoped to the original authenticated controller and route. */
112
+ export function deliveryReceiptStatus(messageId: string, scope: DeliveryScope, now = Date.now()) {
113
+ const record = load()[messageId], saved = record?.scope;
114
+ if (!saved || saved.room !== scope.room || saved.peerPublicKey !== scope.peerPublicKey
115
+ || saved.sessionId !== scope.sessionId || record.updatedAt < now - MAX_AGE_MS
116
+ || record.updatedAt > now || record.state === 'processing' && !record.processingAcknowledged) return undefined;
117
+ return { status: record.rejectedError ? 'rejected' as const : 'accepted' as const, error: record.rejectedError };
118
+ }
119
+
106
120
  /** Release a failed attempt so a relay retry can safely try again. */
107
121
  export function abandonDelivery(messageId: string, now = Date.now()): void {
108
122
  const records = load();
@@ -25,6 +25,13 @@ reports are retained in a bounded private computer cache across restarts, served
25
25
  while a new observation runs, and refreshed after the observation expires. Git
26
26
  admission revision (`sourceRevision`) remains separate from Weavatrix's content
27
27
  fingerprint (`revision`). Failed analyses cannot erase the durable last report.
28
+ Explicit Graph builds also invoke the Engine's `graph.analyze_project` for two
29
+ to eight admitted local repositories. The projection labels these shared static
30
+ contracts and keeps their runtime destination unverified. They do not become
31
+ verified Backbone dependencies or merge Project grants. The Apple Silicon
32
+ artifact pins Engine 0.1.3 with Weavatrix 2.17.5; unsupported Engine operations
33
+ leave this analysis unavailable. See `runtime/project-relationships/README.md`.
34
+
28
35
  Claude, Codex, and Cursor hooks can evaluate the engine's local Project
29
36
  Governance policy only when both `GRANTTAP_ENGINE_ENABLED` and
30
37
  `GRANTTAP_PROJECT_POLICY_ENABLED` are enabled. Project `DENY` and `ASK` precede
@@ -8,8 +8,8 @@ export type EngineArtifact = {
8
8
 
9
9
  /** Release hashes are part of the reviewed MCP, never supplied by a remote manifest. */
10
10
  export const ENGINE_ARTIFACTS: readonly EngineArtifact[] = [
11
- { version: "0.1.2", platform: "darwin", arch: "arm64",
12
- sha256: "fa23668f2f2cbb716a0ba66c5a6e027a25ed4e7e2dedf0c870edb32199045b97", bytes: 12101408 },
11
+ { version: "0.1.3", platform: "darwin", arch: "arm64",
12
+ sha256: "d948b6cfc572fa062af602c72d7b0e14bedcc5b48ecb8fb8d7af5b6fc33e9920", bytes: 12185568 },
13
13
  { version: "0.1.2", platform: "darwin", arch: "x64",
14
14
  sha256: "b3235a15936a77ec323d5ecbc54a0bc19994fe6e5a36044da964b68524b4c1dd", bytes: 13944984 },
15
15
  { version: "0.1.2", platform: "linux", arch: "x64",
@@ -0,0 +1,12 @@
1
+ # Project relationship wire contract
2
+
3
+ `index.ts` validates the Engine's bounded static contract report. Repository IDs
4
+ must belong to its selected set; locations are relative paths in the source
5
+ repository, and the relation remains `contract_match`. Source excerpts,
6
+ absolute paths, foreign identities and unexpected fields fail closed.
7
+
8
+ The public framed-client and parser tests live with the consuming feature in
9
+ `../../runtime/project-relationships/tests/`. Runtime projection is documented
10
+ in that feature's README.
11
+
12
+ License: GrantTap Commercial Source License. See the repository-root `LICENSE`.
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { EngineProtocolError } from "../transport/protocol-base";
3
+
4
+ const text = z.string().min(1).max(512).refine(value => !/[\x00-\x1f\x7f]/.test(value));
5
+ const location = z.object({
6
+ repository_id: text,
7
+ path: text.refine(value => !/^(?:[A-Za-z]:|[\\/])/.test(value)
8
+ && !value.split(/[\\/]/).includes("..")),
9
+ line: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
10
+ }).strict();
11
+ const schema = z.object({
12
+ project_id: text.max(128),
13
+ weavatrix_version: text.max(64),
14
+ analysis_status: z.enum(["COMPLETE", "INCOMPLETE"]),
15
+ repositories: z.array(z.object({ repository_id: text, revision: text }).strict()).min(2).max(8),
16
+ matches: z.array(z.object({
17
+ source: text,
18
+ target: text,
19
+ transport: z.enum(["http", "graphql", "grpc", "event"]),
20
+ relation: z.literal("contract_match"),
21
+ evidence_count: z.number().int().positive().max(1_000_000),
22
+ evidence: z.array(location).min(1).max(16),
23
+ }).strict()).max(128),
24
+ truncated: z.boolean(),
25
+ }).strict().superRefine((value, context) => {
26
+ const ids = new Set(value.repositories.map(item => item.repository_id));
27
+ if (ids.size !== value.repositories.length || value.matches.some(edge =>
28
+ edge.source === edge.target || !ids.has(edge.source) || !ids.has(edge.target)
29
+ || edge.evidence_count < edge.evidence.length
30
+ || edge.evidence.some(item => item.repository_id !== edge.source))) {
31
+ context.addIssue({ code: "custom", message: "relationship scope is invalid" });
32
+ }
33
+ });
34
+
35
+ export type EngineProjectRelationships = z.infer<typeof schema>;
36
+
37
+ export function parseProjectRelationships(value: unknown): void {
38
+ if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536 || !schema.safeParse(value).success) {
39
+ throw new EngineProtocolError("Project relationship evidence is invalid");
40
+ }
41
+ }
@@ -1,3 +1,4 @@
1
+ import type { EngineProjectRelationships } from "../project-relationships";
1
2
  import type {
2
3
  EnginePolicyOperation,
3
4
  EnginePolicyResult,
@@ -105,6 +106,7 @@ export type EngineOperation = EnginePolicyOperation | InvocationEngineOperation
105
106
  | { operation: "project.get"; input: { project_id: string } }
106
107
  | { operation: "project.list"; input: { after_project_id?: string; limit: number } }
107
108
  | { operation: "project.list_bindings"; input: { project_id: string } }
109
+ | { operation: "graph.analyze_project"; input: {project_id:string;repository_ids:string[]} }
108
110
  | { operation: "graph.get_backbone"; input: { project_id: string } }
109
111
  | { operation: "graph.analyze_repository"; input: {
110
112
  project_id: string; repository_id: string;
@@ -136,6 +138,7 @@ export type EngineResult = EnginePolicyResult | InvocationEngineResult | MemoryE
136
138
  projects: EngineProject[]; next_after_project_id: string | null;
137
139
  } }
138
140
  | { operation: "project.bindings"; bindings: EngineProjectBinding[] }
141
+ | { operation: "graph.project_relationships"; graph:EngineProjectRelationships }
139
142
  | { operation: "graph.backbone"; backbone: EngineProjectBackbone }
140
143
  | { operation: "graph.repository"; graph: EngineRepositoryGraph }
141
144
  | { operation: "context.compiled"; compilation: EngineContextCompilation }
@@ -1,3 +1,4 @@
1
+ import { parseProjectRelationships } from "../project-relationships";
1
2
  import { parseInvocationResult } from "../engine-invocation-protocol";
2
3
  import { parseMemoryResult } from "../engine-memory-protocol";
3
4
  import { parsePolicyResult } from "../engine-policy-protocol";
@@ -47,6 +48,7 @@ function parseResult(value: unknown): EngineResult {
47
48
  else if (operation === "project.listed") parseProjectCatalog(result.page);
48
49
  else if (operation === "project.bindings") parseBindings(result.bindings);
49
50
  else if (operation === "project.binding_upserted") parseBinding(result.binding);
51
+ else if (operation === "graph.project_relationships") parseProjectRelationships(result.graph);
50
52
  else if (operation === "graph.backbone") parseBackbone(result.backbone);
51
53
  else if (operation === "graph.repository") parseRepositoryGraph(result.graph);
52
54
  else if (operation === "context.compiled") {
@@ -201,6 +201,7 @@ function expectedResultOperation(
201
201
  if (operation === "graph.get_backbone") return "graph.backbone";
202
202
  if (operation === "graph.get_heads") return "graph.heads";
203
203
  if (operation === "graph.compute_impact") return "graph.impact";
204
+ if (operation === "graph.analyze_project") return "graph.project_relationships";
204
205
  if (operation === "graph.analyze_repository") return "graph.repository";
205
206
  if (operation === "context.compile_project") return "context.compiled";
206
207
  if (operation === "invocation.observe") return "invocation.observed";
@@ -0,0 +1,19 @@
1
+ # Project relationship projection
2
+
3
+ `projectRelationshipPeers` connects the Engine's `graph.analyze_project` to the
4
+ existing Graph refresh. Only distinct repository IDs from admitted local
5
+ Project bindings are selected, with a limit of eight. A separate Engine socket
6
+ keeps expensive analysis off the policy channel.
7
+
8
+ Explicit Graph builds refresh static contract matches. Background snapshots
9
+ read the bounded retained result for the same binding/revision key; they do not
10
+ start additional cross-repository scans. Stale or unavailable results do not
11
+ establish a missing dependency.
12
+
13
+ The existing compatibility graph shows these edges as shared contracts, with
14
+ an explicit static/runtime-unverified explanation. They never enter the
15
+ verified Backbone and never merge independent Project groups or change grants.
16
+ Full relative source evidence stays in the typed Engine response. No source
17
+ excerpts or absolute machine roots are published to the phone.
18
+
19
+ License: GrantTap Commercial Source License. See the repository-root `LICENSE`.
@@ -0,0 +1,71 @@
1
+ import { join } from "node:path";
2
+ import type { IntegrationPeer, ProjectBindingSummary } from "../../../../../../packages/protocol/schema";
3
+ import type { EngineProjectRelationships } from "../../protocol/project-relationships";
4
+ import { configDir } from "../../../config/runtime/paths";
5
+ import { EngineClient } from "../engine-client";
6
+ import { engineFeatureEnabled, type EngineClientLike } from "../engine-supervisor";
7
+ import { RepositoryGraphJobs } from "../repository-graph-jobs";
8
+
9
+ type Analysis = { graph: EngineProjectRelationships; observedAt: number };
10
+ const jobs = new RepositoryGraphJobs<Analysis>(Date.now, 120_000, 300_000, 8);
11
+ const STATIC_CONTRACT_PREFIX = "Weavatrix: static ";
12
+
13
+ /** Static compatibility is shown as shared contracts, never a proven dependency. */
14
+ export async function projectRelationshipPeers(
15
+ projectId: string,
16
+ bindings: ProjectBindingSummary[],
17
+ options: { env?: NodeJS.ProcessEnv; client?: EngineClientLike; background?: boolean } = {},
18
+ ): Promise<IntegrationPeer[]> {
19
+ if (!engineFeatureEnabled(options.env ?? process.env)) return [];
20
+ const ids = [...new Set(bindings.map(binding => binding.repositoryId))].sort();
21
+ if (ids.length < 2 || ids.length > 8) return [];
22
+ const key = JSON.stringify([
23
+ projectId, ids, bindings.map(item => [item.bindingId, item.revision]).sort(),
24
+ ]);
25
+ // Background snapshots read retained results; explicit Graph builds start analysis.
26
+ const result = options.background
27
+ ? jobs.cached(key)
28
+ : await jobs.refresh(key, () => analyze(projectId, ids, options.client));
29
+ if (!result) return [];
30
+ return result.graph.matches.flatMap(edge => {
31
+ if (edge.target.length > 160) return [];
32
+ return [{
33
+ projectId,
34
+ repositoryId: edge.source,
35
+ peer: edge.target,
36
+ via: edge.transport === "event" ? "kafka" as const : "api" as const,
37
+ relation: "shares" as const,
38
+ through: `${STATIC_CONTRACT_PREFIX}${edge.transport.toUpperCase()} contract; runtime target unverified`,
39
+ updatedAt: result.observedAt,
40
+ }];
41
+ }).slice(0, 64);
42
+ }
43
+
44
+ async function analyze(
45
+ projectId: string, ids: string[], supplied?: EngineClientLike,
46
+ ): Promise<Analysis | undefined> {
47
+ const client = supplied ?? new EngineClient({
48
+ socketPath: join(configDir(), "engine.sock"), connectTimeoutMs: 10_000,
49
+ });
50
+ try {
51
+ const result = await client.request({
52
+ operation: "graph.analyze_project",
53
+ input: { project_id: projectId, repository_ids: ids },
54
+ }, { timeoutMs: 120_000 });
55
+ if (result.operation !== "graph.project_relationships" || result.graph.project_id !== projectId
56
+ || JSON.stringify(result.graph.repositories.map(item => item.repository_id).sort()) !== JSON.stringify(ids)) {
57
+ return undefined;
58
+ }
59
+ return { graph: result.graph, observedAt: Date.now() };
60
+ } catch { return undefined; }
61
+ finally { if (!supplied) client.close(); }
62
+ }
63
+
64
+ export function mergeRelationshipPeers(
65
+ current: IntegrationPeer[] | undefined, analyzed: IntegrationPeer[],
66
+ ): IntegrationPeer[] {
67
+ return [
68
+ ...(current ?? []).filter(peer => !peer.through?.startsWith(STATIC_CONTRACT_PREFIX)),
69
+ ...analyzed,
70
+ ].slice(0, 64);
71
+ }
@@ -56,6 +56,12 @@ export class RepositoryGraphJobs<T> {
56
56
  return new Promise(resolve => entry.listeners.push(resolve));
57
57
  }
58
58
 
59
+ /** Read a fresh in-memory result without starting or queueing an analysis. */
60
+ cached(key: string): T | undefined {
61
+ const entry = this.entries.get(key);
62
+ return entry && this.now() < entry.retryAt ? entry.value : undefined;
63
+ }
64
+
59
65
  private admit(priority: number): boolean {
60
66
  if (this.queue.length < this.capacity) return true;
61
67
  const last = this.queue[this.queue.length - 1];
@@ -50,11 +50,14 @@ function taskOrderKey(task: TaskValue): string {
50
50
  export function preferExecution(
51
51
  current: ExecutionValue,
52
52
  incoming: ExecutionValue,
53
+ preparedOwner = false,
53
54
  ): ExecutionValue {
54
55
  if ((current.endedAt != null) !== (incoming.endedAt != null)) {
55
56
  const ended = current.endedAt != null ? current : incoming;
56
57
  const live = current.endedAt != null ? incoming : current;
57
58
  if (live.continuedAt != null && live.continuedAt > ended.endedAt!) return live;
59
+ if (preparedOwner && /^transfer-[a-f0-9]{48}$/.test(live.sessionId)
60
+ && (live.updatedAt ?? live.startedAt) > ended.endedAt!) return live;
58
61
  return { ...live, endedAt: ended.endedAt };
59
62
  }
60
63
  const currentAt = current.updatedAt ?? current.startedAt;
@@ -78,7 +81,10 @@ export function mayOwnTask(
78
81
  sessionId: string,
79
82
  executions: readonly ExecutionValue[],
80
83
  ): boolean {
81
- if (task.ownerSessionId == null || task.ownerSessionId === sessionId) return true;
84
+ if (task.ownerSessionId === sessionId) return true;
85
+ const candidate = executions.find(item => item.taskId === task.taskId && item.sessionId === sessionId);
86
+ if (candidate?.endedAt != null) return false;
87
+ if (task.ownerSessionId == null) return true;
82
88
  const owner = executions.find((item) =>
83
89
  item.taskId === task.taskId && item.sessionId === task.ownerSessionId);
84
90
  return owner == null || owner.endedAt != null;
@@ -19,6 +19,7 @@ import { readIntegrationMap } from "../observed/integration-map";
19
19
  import type { MeshStore } from "../store";
20
20
  import { queueProjectBindingSync } from "../../engine/runtime/engine-projects";
21
21
  import { isCursorTaskCloneId } from "../../sessions/cursor/catalog";
22
+ import { localTaskTransfers } from '../../task-transfer';
22
23
  export { inspectRepositoryAsync } from "./repository-inspection";
23
24
 
24
25
  export type RepositoryFacts = {
@@ -122,6 +123,7 @@ export function linkSessionsToProjects(
122
123
  inspect: (cwd: string) => RepositoryFacts = inspectRepository,
123
124
  formerNames: string[] = formerComputerNames(),
124
125
  ): SessionInfo[] {
126
+ localTaskTransfers().reconcileCatalogOwnership(store, computerId);
125
127
  // Records kept under a name this computer used to go by are this computer's
126
128
  // leftovers, not a second machine: their executions are over and their
127
129
  // bindings unavailable.
@@ -27,6 +27,8 @@ export function closeVanished(
27
27
  if (execution.computerId !== computerId || execution.endedAt != null) continue;
28
28
  // A provider cloud run is never a local chat, so its absence here says nothing.
29
29
  if (execution.external) continue;
30
+ // Prepared aliases are owned by the durable transfer catalog, not a native CLI scan.
31
+ if (/^transfer-[a-f0-9]{48}$/.test(execution.sessionId)) continue;
30
32
  if (!scannedProviders.has(execution.provider)) continue;
31
33
  if (liveSessionIds.has(execution.sessionId)) continue;
32
34
  execution.endedAt = endedAt;
@@ -18,6 +18,13 @@ computer analyzes only its own endpoint bindings. Failed bindings remain visible
18
18
  projection and phone refresh use this rule. The graph contains actual Engine
19
19
  analysis; unavailable analysis is never synthesized as a successful graph.
20
20
 
21
+ After repository analysis, explicit builds request bounded cross-repository
22
+ contract matches from the same admitted bindings. These are projected as shared
23
+ static contracts with an unverified runtime target. Background snapshots reuse
24
+ fresh in-memory results; they do not queue additional multi-repository scans.
25
+ A verified Backbone remains authoritative. Project identities and grants are
26
+ preserved.
27
+
21
28
  Behavior tests: `tests/`, root `tests/graph/graph-binding-sync.test.ts`, and
22
29
  `apps/mcp/src/desktop/projection/tests/graph-refresh.test.ts`. The optional
23
30
  `engine-selection-live.test.ts` uses `GRANTTAP_ENGINE_TEST_BINARY` to verify the
@@ -1,3 +1,4 @@
1
+ import { projectRelationshipPeers, mergeRelationshipPeers } from "../../../engine/runtime/project-relationships";
1
2
  import type { RelayClient } from "../../../../../../packages/core/relay-client";
2
3
  import type { MeshSnapshot } from "../../../../../../packages/protocol/schema";
3
4
  import { projectBackbone, projectRepositoryGraphs, waitForProjectBindingSync } from "../../../engine/runtime/engine-projects";
@@ -17,6 +18,7 @@ type GraphAnalysisDependencies = {
17
18
  sync?: typeof refreshLocalGraphBindings;
18
19
  backbone: typeof projectBackbone;
19
20
  graphs: typeof projectRepositoryGraphs;
21
+ relationships?: typeof projectRelationshipPeers;
20
22
  send: (client: RelayClient, snapshot: MeshSnapshot) => Promise<void>;
21
23
  endpoint?: () => string;
22
24
  details?: (snapshot: MeshSnapshot) => ReturnType<typeof repositoryDetails>;
@@ -28,6 +30,7 @@ const graphAnalysisDependencies: GraphAnalysisDependencies = {
28
30
  sync: refreshLocalGraphBindings,
29
31
  backbone: projectBackbone,
30
32
  graphs: projectRepositoryGraphs,
33
+ relationships: projectRelationshipPeers,
31
34
  endpoint: computerId,
32
35
  details: snapshot => repositoryDetails(snapshot, computerId()),
33
36
  send: (client, snapshot) => sendMeshPayload(client, snapshot, "phone", {
@@ -61,6 +64,9 @@ export async function analyzeProjectGraphNow(
61
64
  dependencies.graphs(projectId, bindings, { background: false }),
62
65
  dependencies.details?.(current),
63
66
  ]);
67
+ const relationships = await dependencies.relationships?.(projectId, bindings, { background: false });
68
+ const peers = relationships === undefined ? current.peers
69
+ : mergeRelationshipPeers(current.peers, relationships);
64
70
  const observed = [...repositoryGraphs, ...failures];
65
71
  const reports = observed.length > 0 ? observed : [{
66
72
  projectId, repositoryId: current.project.canonicalRepositoryId,
@@ -71,7 +77,7 @@ export async function analyzeProjectGraphNow(
71
77
  const receipts = requestIds?.size ? requestIds : new Set<string | undefined>([undefined]);
72
78
  for (const id of receipts) {
73
79
  await dependencies.send(client, {
74
- ...current, backbone, repositoryGraphs: reports, generatedAt: Date.now(),
80
+ ...current, backbone, peers, repositoryGraphs: reports, generatedAt: Date.now(),
75
81
  ...(details ? { repositoryDetails: details } : {}),
76
82
  ...(id ? { graphAnalysisRequestId: id } : {}),
77
83
  });
@@ -1,3 +1,4 @@
1
+ import { projectRelationshipPeers, mergeRelationshipPeers } from "../../../engine/runtime/project-relationships";
1
2
  import { refreshLocalGraphBindings } from "../graph/binding-sync";
2
3
  import { graphAnalysisBindings } from "../graph/checkout-identity";
3
4
  import type { MeshSnapshot } from "../../../../../../packages/protocol/schema";
@@ -33,6 +34,10 @@ export async function enrichMeshSnapshot(
33
34
  projectKnowledge(snapshot.projectId),
34
35
  repositoryDetails(snapshot, computerId()),
35
36
  ]);
37
+ const relationships = await projectRelationshipPeers(snapshot.projectId, bindings, {
38
+ background: options.refreshGraph !== true,
39
+ });
40
+ const peers = mergeRelationshipPeers(snapshot.peers, relationships);
36
41
  const sharedKnowledge = knowledge?.filter((item) => item.visibility === "project");
37
42
  if (sharedKnowledge) localMeshStore().cacheKnowledge(snapshot.projectId, sharedKnowledge);
38
43
  const memoryRows = new Map((snapshot.knowledge ?? []).map((item) => [item.recordId, item]));
@@ -42,7 +47,7 @@ export async function enrichMeshSnapshot(
42
47
  ...(snapshot.supersededKnowledgeRecordIds ?? []),
43
48
  ...supersededProjectKnowledgeIds(snapshot.projectId, allMemory),
44
49
  ])].sort().slice(-128);
45
- const enriched = { ...snapshot, backbone, ...(repositoryGraphs.length || identityFailures.length
50
+ const enriched = { ...snapshot, backbone, peers, ...(repositoryGraphs.length || identityFailures.length
46
51
  ? { repositoryGraphs: [...repositoryGraphs, ...identityFailures] } : {}),
47
52
  repositoryDetails: details,
48
53
  knowledge: activeProjectKnowledge(snapshot.projectId, allMemory)
@@ -4,6 +4,7 @@ import { upsertBinding } from "../catalog/binding-state";
4
4
  import { integrationPeerKey } from "../handoff/other-side";
5
5
  import { mergeBy, mergeWith } from "../store/support";
6
6
  import { MAX_STORE_PEERS, type StoreState } from "../store/state";
7
+ import { preferScopedExecution } from '../store/receipts/execution-merge';
7
8
 
8
9
  export function mergeSnapshotState(state: StoreState, input: SnapshotValue): void {
9
10
  const snapshot = MeshSnapshot.parse(input);
@@ -25,11 +26,12 @@ export function mergeSnapshotState(state: StoreState, input: SnapshotValue): voi
25
26
  candidate.peers = mergeBy(candidate.peers, snapshot.peers ?? [], integrationPeerKey)
26
27
  .slice(-MAX_STORE_PEERS);
27
28
  candidate.tasks = mergeWith(candidate.tasks, snapshot.tasks, (item) => item.taskId, preferTask);
29
+ candidate.events = mergeBy(candidate.events, snapshot.events, (item) => item.eventId).slice(-512);
28
30
  candidate.executions = mergeWith(
29
31
  candidate.executions,
30
32
  snapshot.executions,
31
33
  (item) => `${item.computerId}\0${item.provider}\0${item.sessionId}`,
32
- preferExecution,
34
+ (current, incoming) => preferScopedExecution(candidate, current, incoming),
33
35
  );
34
36
  candidate.claims = mergeBy(candidate.claims, snapshot.claims, (item) => item.claimId);
35
37
  candidate.dependencies = mergeBy(
@@ -37,7 +39,6 @@ export function mergeSnapshotState(state: StoreState, input: SnapshotValue): voi
37
39
  snapshot.dependencies,
38
40
  (item) => `${item.taskId}\0${item.dependsOnTaskId}`,
39
41
  );
40
- candidate.events = mergeBy(candidate.events, snapshot.events, (item) => item.eventId).slice(-512);
41
42
  for (const incoming of snapshot.knowledge ?? []) {
42
43
  const known = candidate.knowledge.find((item) => item.projectId === incoming.projectId
43
44
  && item.recordId === incoming.recordId);
@@ -28,14 +28,14 @@ import { projectSharedSkills } from "../../capabilities/skills";
28
28
  import { loadExecutionPolicy } from "../runtime/execution-policy";
29
29
  import { loadEnvironment, redactEnvironment } from "../context/env";
30
30
  import { loadRestrictions } from "../restrictions";
31
- import { MeshStoreBase } from "./base";
31
+ import { MeshPreparedStore } from "./receipts/prepared-availability";
32
32
  import { activeProjectKnowledge, supersededProjectKnowledgeIds } from "../knowledge/active";
33
33
  import { restoreHandoffResult, validStoredReceipt } from './receipts';
34
34
  import {
35
35
  projectCapabilityRequests,
36
36
  } from "../catalog/project/requests";
37
37
 
38
- export class MeshStore extends MeshStoreBase {
38
+ export class MeshStore extends MeshPreparedStore {
39
39
  recordReceipt(input: ReceiptValue): void {
40
40
  this.sync();
41
41
  const receipt = HandoffReceipt.parse(input);
@@ -0,0 +1,10 @@
1
+ import type { ExecutionSessionLink } from '../../../../../../packages/protocol/schema';
2
+ import { isTerminalTaskState, preferExecution } from '../../admin/convergence';
3
+ import type { StoreState } from '../state';
4
+
5
+ /** Ownership has already converged before availability; an ended former owner never qualifies. */
6
+ export function preferScopedExecution(state: StoreState, current: ExecutionSessionLink, incoming: ExecutionSessionLink) {
7
+ const task = state.tasks.find(row => row.taskId === incoming.taskId);
8
+ const preparedOwner = !!task && !isTerminalTaskState(task.state) && task.ownerSessionId === incoming.sessionId;
9
+ return preferExecution(current, incoming, preparedOwner);
10
+ }
@@ -0,0 +1,55 @@
1
+ import type { PreparedTransfer } from '../../../task-transfer';
2
+ import { isTerminalTaskState, nextTaskRevision } from '../../admin/convergence';
3
+ import { MeshStoreBase } from '../base';
4
+ import { validStoredReceipt } from './index';
5
+ import { capsuleHash } from '../../handoff';
6
+ import type { StoreState } from '../state';
7
+
8
+ /** Repairs only an unstarted, durably accepted local preparation. */
9
+ export class MeshPreparedStore extends MeshStoreBase {
10
+ reconcilePreparedTransfers(rows: PreparedTransfer[], verify: (row: PreparedTransfer) => boolean): number {
11
+ this.sync();
12
+ if (!rows.some(row => repairable(this.state, row))) return 0;
13
+ const result = this.transact(() => {
14
+ let repaired = 0;
15
+ for (const row of rows) {
16
+ const facts = repairable(this.state, row);
17
+ if (!facts || !verify(row)) continue;
18
+ const at = Math.max(this.now(), (facts.destination.endedAt ?? 0) + 1, facts.task.updatedAt);
19
+ facts.destination.endedAt = undefined;
20
+ facts.destination.updatedAt = at;
21
+ facts.task.ownerSessionId = row.sessionId;
22
+ facts.task.state = 'planned';
23
+ facts.task.updatedAt = at;
24
+ facts.task.revision = nextTaskRevision(facts.task);
25
+ repaired += 1;
26
+ }
27
+ return repaired;
28
+ });
29
+ return result.applied ? result.value : 0;
30
+ }
31
+ }
32
+
33
+ function repairable(state: StoreState, row: PreparedTransfer) {
34
+ if (row.phase !== 'prepared' || row.nativeSessionId) return undefined;
35
+ const task = state.tasks.find(item => item.taskId === row.capsule.taskId && item.projectId === row.projectId);
36
+ if (!task || isTerminalTaskState(task.state)
37
+ || ![row.sourceSessionId, row.sessionId].includes(task.ownerSessionId ?? '')) return undefined;
38
+ const destination = state.executions.find(item => item.taskId === task.taskId && item.sessionId === row.sessionId
39
+ && item.provider === row.capsule.targetProvider && item.computerId === row.capsule.targetComputer
40
+ && item.workspace === row.workspace && item.startedAt === row.acceptedAt);
41
+ if (!destination || destination.endedAt == null && task.ownerSessionId === row.sessionId) return undefined;
42
+ const request = state.events.find(item => item.eventId === row.eventId && item.eventType === 'HANDOFF_REQUEST'
43
+ && item.taskId === task.taskId && item.projectId === row.projectId && item.sourceSessionId === row.sourceSessionId);
44
+ if (!request || state.events.some(item => item.taskId === task.taskId && item.eventType === 'HANDOFF_REQUEST'
45
+ && item.eventId !== row.eventId && item.createdAt >= request.createdAt)) return undefined;
46
+ const accepted = state.events.find(item => item.eventType === 'HANDOFF_ACCEPTED' && item.taskId === task.taskId
47
+ && item.projectId === row.projectId && item.sourceSessionId === row.sessionId
48
+ && item.payload.receipt?.prepared === true && item.payload.receipt.targetSessionId === row.sessionId
49
+ && item.payload.receipt.sourceSessionId === row.sourceSessionId && item.payload.receipt.acceptedAt >= row.acceptedAt
50
+ && item.payload.receipt.capsuleHash === capsuleHash(row.capsule));
51
+ const source = state.executions.find(item => item.taskId === task.taskId && item.sessionId === row.sourceSessionId);
52
+ if (!accepted || !validStoredReceipt(state, accepted) || !source?.endedAt
53
+ || source.continuedAt != null && source.continuedAt >= row.acceptedAt) return undefined;
54
+ return { task, destination };
55
+ }
@@ -19,6 +19,7 @@ import type {
19
19
  MeshTask as TaskValue,
20
20
  } from "../../../../../packages/protocol/schema";
21
21
  import { preferExecution, preferTask } from "../admin/convergence";
22
+ import { preferScopedExecution } from './receipts/execution-merge';
22
23
  import { integrationPeerKey } from "../handoff/other-side";
23
24
  import {
24
25
  MAX_RELEASED_CLAIMS, MAX_STORE_MIGRATIONS, MAX_STORE_PEERS, type StoreState,
@@ -115,7 +116,7 @@ export function applyStoreDelta(disk: StoreState, delta: StoreDelta): StoreState
115
116
  const key = rowKey(name, item);
116
117
  const theirs = rows.get(key);
117
118
  const untouched = theirs == null || JSON.stringify(theirs) === before;
118
- rows.set(key, untouched ? item : settle(name, theirs, item));
119
+ rows.set(key, untouched ? item : settle(name, theirs, item, merged as StoreState));
119
120
  }
120
121
  let items = [...rows.values()];
121
122
  const bound = BOUNDS[name];
@@ -125,9 +126,9 @@ export function applyStoreDelta(disk: StoreState, delta: StoreDelta): StoreState
125
126
  return merged as StoreState;
126
127
  }
127
128
 
128
- function settle(name: Collection, theirs: unknown, ours: unknown): unknown {
129
+ function settle(name: Collection, theirs: unknown, ours: unknown, state: StoreState): unknown {
129
130
  if (name === "tasks") return preferTask(theirs as TaskValue, ours as TaskValue);
130
- if (name === "executions") return preferExecution(theirs as ExecutionValue, ours as ExecutionValue);
131
+ if (name === "executions") return preferScopedExecution(state, theirs as ExecutionValue, ours as ExecutionValue);
131
132
  if (name === "knowledge") return theirs;
132
133
  return ours;
133
134
  }
@@ -44,6 +44,7 @@ import { requestProjectGraphRefresh } from "../graph-refresh";
44
44
  import { handleControllerPairRequest, handleInboundKnowledgeWrite } from "../controller-knowledge";
45
45
  import { acceptPhoneAccountLink } from "../../account-recovery/phone-link";
46
46
  import { approvalsStatus } from '../../approvals/state';
47
+ import { handleDeliveryReceiptQuery } from '../message-delivery/status-query';
47
48
 
48
49
  export async function handleMonitorMessage(
49
50
  client: RelayClient,
@@ -70,6 +71,8 @@ export async function handleMonitorMessage(
70
71
  }
71
72
  if (payload.type === "user.message") {
72
73
  return handleInboundUserMessage(client, payload, publish, input.peerPublicKey);
74
+ } else if (payload.type === 'delivery.receipt.query') {
75
+ return handleDeliveryReceiptQuery(client, payload, input.peerPublicKey);
73
76
  } else if (payload.type === "user.attachment") {
74
77
  // Ahead of its message, so the message itself travels light.
75
78
  return storeAttachment(payload, client.room);
@@ -22,7 +22,9 @@ export async function sendDeliveryReceipt(
22
22
  receivedAt: Date.now(),
23
23
  };
24
24
  const options = { ttlMs: 24 * 60 * 60_000, peerPublicKey };
25
- await (sessionId
25
+ // A controller receipt is control-plane state. It remains device-encrypted,
26
+ // and must not depend on a separately retained task-key grant arriving first.
27
+ await (sessionId && !peerPublicKey
26
28
  ? sendSessionPayload(client, payload, sessionId, "phone", options)
27
29
  : client.send(payload, "phone", options)).catch(() => {});
28
30
  }
@@ -137,7 +137,7 @@ export async function handleUserMessage(
137
137
  const resolved = resolveMessageAttachments(message, (attachmentId) => takeAttachment(attachmentId, client.room));
138
138
  if (!resolved.ok) {
139
139
  if (message.messageId) {
140
- await sendDeliveryReceipt(client, message.messageId, "rejected", ATTACHMENT_MISSING_ERROR, message.sessionId);
140
+ await sendDeliveryReceipt(client, message.messageId, "rejected", ATTACHMENT_MISSING_ERROR, message.sessionId, peerPublicKey);
141
141
  }
142
142
  return "rejected";
143
143
  }
@@ -16,3 +16,9 @@ This module uses the repository GrantTap Commercial Source License.
16
16
  Replies and receipts target the requesting controller. A provider refusal after
17
17
  the processing acknowledgement emits a rejected receipt and retains that final
18
18
  outcome across ciphertext retries; it never silently turns back into accepted.
19
+
20
+ `delivery.receipt.query` reads the original durable result without sending a
21
+ message again. Only the original authenticated controller, room, and session
22
+ may read it. Older unscoped records are not exposed through this query.
23
+ Receipts to a known controller use its device encryption directly, so a missed
24
+ task-key grant cannot hide a provider rejection or processing acknowledgement.
@@ -14,7 +14,8 @@ export async function handleInboundUserMessage(
14
14
  if (payload.requestId) return false;
15
15
  let deliveryStarted = false;
16
16
  if (payload.messageId) {
17
- const state = beginDelivery(payload.messageId);
17
+ const state = beginDelivery(payload.messageId, Date.now(), payload.sessionId && peerPublicKey
18
+ ? { room: client.room, sessionId: payload.sessionId, peerPublicKey } : undefined);
18
19
  if (state !== "started") {
19
20
  if (state === "completed" || processingDeliveryAcknowledged(payload.messageId)) {
20
21
  const failure = completedDeliveryFailure(payload.messageId);
@@ -0,0 +1,12 @@
1
+ import type { RelayClient } from '../../../../../packages/core/relay-client';
2
+ import type { DeliveryReceiptQuery } from '../../../../../packages/protocol/schema';
3
+ import { deliveryReceiptStatus } from '../../delivery';
4
+ import { sendDeliveryReceipt } from '../handlers/receipts';
5
+
6
+ export async function handleDeliveryReceiptQuery(client: RelayClient, query: DeliveryReceiptQuery,
7
+ peerPublicKey?: string): Promise<boolean> {
8
+ if (!peerPublicKey) return true;
9
+ const receipt = deliveryReceiptStatus(query.messageId, { room: client.room, peerPublicKey, sessionId: query.sessionId });
10
+ if (receipt) await sendDeliveryReceipt(client, query.messageId, receipt.status, receipt.error, query.sessionId, peerPublicKey);
11
+ return true;
12
+ }
@@ -0,0 +1,13 @@
1
+ # Native Grok activity
2
+
3
+ `grokNativeActivity` reads at most 64 KiB of the native structured event log.
4
+ It confirms the active registry PID and OS process start time before treating a
5
+ turn as running. Summary writes alone cannot prove that Grok is idle during a
6
+ long turn. Permission phases remain waiting; completed turns are idle.
7
+
8
+ The session catalog consumes these observations, and the resume adapter checks
9
+ them again before launching a headless process. A queued phone message waits
10
+ for the current native turn instead of loading the same running session twice.
11
+ No prompt, reasoning, or native event contents are published by this reader.
12
+
13
+ Licensed under the GrantTap Commercial Source License. See LICENSE and NOTICE.
@@ -0,0 +1,64 @@
1
+ import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ type LiveActivity = { state: 'working' | 'waiting' | 'idle'; updatedAt: number };
7
+
8
+ export function grokResumeActivity(session: { sessionId: string; cwd?: string }): LiveActivity | undefined {
9
+ if (!session.cwd || !/^[a-zA-Z0-9_-]{1,256}$/.test(session.sessionId)) return undefined;
10
+ const root = process.env.GRANTTAP_GROK_SESSIONS_DIR
11
+ ?? join(process.env.GROK_HOME ?? join(homedir(), '.grok'), 'sessions');
12
+ return grokNativeActivity(session.sessionId, join(root, encodeURIComponent(session.cwd), session.sessionId));
13
+ }
14
+
15
+ /** Native events, unlike summary.json, advance during a long running turn. */
16
+ export function grokNativeActivity(sessionId: string, directory: string, now = Date.now()): LiveActivity | undefined {
17
+ const root = process.env.GRANTTAP_GROK_DIR ?? process.env.GROK_HOME ?? join(homedir(), '.grok');
18
+ const owned = ownedSession(root, sessionId);
19
+ const lines = tail(join(directory, 'events.jsonl'));
20
+ for (const line of lines.reverse()) {
21
+ let row: Record<string, unknown>;
22
+ try { row = JSON.parse(line); } catch { continue; }
23
+ const updatedAt = typeof row.ts === 'string' ? Date.parse(row.ts) : NaN;
24
+ if (!Number.isFinite(updatedAt) || updatedAt > now + 5_000) continue;
25
+ if (row.type === 'turn_ended') return { state: 'idle', updatedAt };
26
+ if (row.type === 'turn_started') return owned ? { state: 'working', updatedAt } : undefined;
27
+ if (row.type !== 'phase_changed') continue;
28
+ if (row.phase === 'permission_prompt') return owned ? { state: 'waiting', updatedAt } : undefined;
29
+ if (['waiting_for_model', 'streaming_reasoning', 'streaming_text', 'tool_execution'].includes(String(row.phase))) {
30
+ return owned ? { state: 'working', updatedAt } : undefined;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ function ownedSession(root: string, sessionId: string): boolean {
37
+ try {
38
+ const path = join(root, 'active_sessions.json');
39
+ if (statSync(path).size > 512_000) return false;
40
+ const rows = JSON.parse(readFileSync(path, 'utf8'));
41
+ if (!Array.isArray(rows) || rows.length > 256) return false;
42
+ const row = rows.find(item => item?.session_id === sessionId);
43
+ if (!row || !Number.isSafeInteger(row.pid) || row.pid <= 0 || !Number.isFinite(row.pid_start)) return false;
44
+ process.kill(row.pid, 0);
45
+ // Grok records microseconds. Match the OS start time so a reused PID cannot hold a Task.
46
+ const started = Date.parse(execFileSync('ps', ['-p', String(row.pid), '-o', 'lstart='], {
47
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 500,
48
+ env: { ...process.env, LC_ALL: 'C' }, maxBuffer: 1_024,
49
+ }).trim());
50
+ return Math.abs(started - row.pid_start / 1_000) < 1_000;
51
+ } catch { return false; }
52
+ }
53
+
54
+ function tail(path: string): string[] {
55
+ let fd: number | undefined;
56
+ try {
57
+ const size = statSync(path).size, length = Math.min(size, 64 * 1_024);
58
+ fd = openSync(path, 'r'); const buffer = Buffer.alloc(length);
59
+ const read = readSync(fd, buffer, 0, length, size - length);
60
+ const lines = buffer.subarray(0, read).toString('utf8').split('\n');
61
+ return size > length ? lines.slice(1) : lines;
62
+ } catch { return []; }
63
+ finally { if (fd != null) closeSync(fd); }
64
+ }
@@ -7,6 +7,7 @@ import type { ReplyResult } from "../payload/types";
7
7
  import type { DeliveryOptions } from "../payload/types";
8
8
  import { resolveGrokBinary } from "../../providers/grok-bin";
9
9
  import { runCursorConversation } from "../cursor-conversation";
10
+ import { grokResumeActivity } from '../../providers/grok-activity';
10
11
 
11
12
  function grokBin(): string {
12
13
  return resolveGrokBinary();
@@ -42,6 +43,10 @@ export function runGrokNew(
42
43
  export function runGrokResume(
43
44
  session: SessionInfo, text: string, timeoutMs: number, options: DeliveryOptions = {},
44
45
  ): Promise<ReplyResult> {
46
+ const live = grokResumeActivity(session);
47
+ if (live?.state === 'working' || live?.state === 'waiting') return Promise.resolve({ ok: false,
48
+ error: live.state === 'working' ? 'Grok is still running a turn in this task. Wait for it to finish, then retry this message.'
49
+ : 'Grok is waiting for permission in its open task. Resolve that request, then retry this message.' });
45
50
  const permission = options.permissionMode === "manual" ? "default" : options.permissionMode;
46
51
  const args = [
47
52
  "--no-auto-update", ...(session.cwd ? ["--cwd", session.cwd] : []),
@@ -18,6 +18,7 @@ import {
18
18
  type Scan,
19
19
  } from "../support/common";
20
20
  import type { CapabilityObservation } from "../telemetry";
21
+ import { grokNativeActivity } from '../../providers/grok-activity';
21
22
 
22
23
  type Summary = {
23
24
  info?: { id?: unknown; cwd?: unknown };
@@ -170,10 +171,11 @@ export function scanGrok(maxFiles = MAX_FILES): Scan {
170
171
  const title = [summary.generated_title, summary.session_summary, parsed.firstUser]
171
172
  .find((value): value is string => typeof value === "string" && value.trim().length > 0);
172
173
  const cwd = typeof summary.info?.cwd === "string" ? summary.info.cwd : undefined;
174
+ const live = grokNativeActivity(sessionId, directory);
173
175
  sessions.push({
174
176
  sessionId, agent: "grok", title: title?.trim().slice(0, 120), cwd,
175
177
  model: typeof summary.current_model_id === "string" ? summary.current_model_id : undefined,
176
- state: stateFor(updatedAt), startedAt, lastActivityAt: updatedAt,
178
+ state: live?.state ?? stateFor(updatedAt), startedAt, lastActivityAt: Math.max(updatedAt, live?.updatedAt ?? 0),
177
179
  ...(parsed.lastMessageAt ? { lastMessageAt: parsed.lastMessageAt } : {}),
178
180
  tokensSession: parsed.tokens, tokensLastTurn: parsed.lastTurn,
179
181
  contextTokensUsed: parsed.tokens,
@@ -10,6 +10,13 @@ oversized context refuses preparation instead of silently dropping history.
10
10
  A launch is recorded before invoking the provider. An interrupted first launch
11
11
  requires inspection and cannot automatically create a second native execution.
12
12
 
13
+ Native catalog sweeps never close a prepared alias. The catalog can repair an
14
+ older false closure from the retained accepted receipt and checksummed, unstarted
15
+ preparation. It does not supersede a later transfer, reopen completed work, or
16
+ invent consent to resume the source. Existing Task ownership, the accepted
17
+ receipt, and a newer live observation keep availability through delayed snapshot
18
+ merges without changing the persisted execution format.
19
+
13
20
  Tests: `tests/prepared.test.ts` covers preparation, restart, model/effort forwarding,
14
21
  context persistence, stable catalog identity and ambiguous first submissions.
15
22
 
@@ -2,6 +2,7 @@ import type { CodingAgent, SessionInfo, UserAttachment } from '../../../../packa
2
2
  import type { DeliveryOptions, ReplyResult } from '../reply';
3
3
  import { TransferPersistence } from './persistence';
4
4
  import type { PreparedTransfer } from './records';
5
+ import type { MeshStore } from '../mesh/store';
5
6
  export type { PreparedTransfer, TransferPreparation } from './records';
6
7
 
7
8
  type TransferProvider = {
@@ -14,6 +15,12 @@ const interrupted = (): ReplyResult => ({ ok: false,
14
15
 
15
16
  /** Prepared executions are visible immediately and start only with a person's first message. */
16
17
  export class PreparedTaskTransfers extends TransferPersistence {
18
+ reconcileCatalogOwnership(store: MeshStore, computerId: string): number {
19
+ return store.reconcilePreparedTransfers(this.records().filter(row => row.capsule.targetComputer === computerId), row => {
20
+ try { this.readContext(row); return true; } catch { return false; }
21
+ });
22
+ }
23
+
17
24
  catalog(native: SessionInfo[]): SessionInfo[] {
18
25
  const rows = this.records().map(row => {
19
26
  if (row.phase !== 'starting' || ownerAlive(row.ownerPid)) return row;
@@ -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.95
13
+ npm install -g granttap-mcp@0.8.97
14
14
  granttap setup
15
15
  granttap status
16
16
  ```
@@ -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.95\";\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 22.13.0+ 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"
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.97\";\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 22.13.0+ 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.95";
10
+ const PACKAGE = "granttap-mcp@0.8.97";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.95",
3
+ "version": "0.8.97",
4
4
  "description": "Commercial computer runtime connecting local coding agents to GrantTap on Mac, iPhone, iPad and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -110,6 +110,13 @@ export const DeliveryReceipt = z.object({
110
110
  });
111
111
  export type DeliveryReceipt = z.infer<typeof DeliveryReceipt>;
112
112
 
113
+ /** Reads the original operation; it cannot submit or resume a provider turn. */
114
+ export const DeliveryReceiptQuery = z.object({
115
+ type: z.literal('delivery.receipt.query'), messageId: z.string().min(1).max(180),
116
+ sessionId: z.string().min(1).max(256), createdAt: z.number().finite(),
117
+ }).strict();
118
+ export type DeliveryReceiptQuery = z.infer<typeof DeliveryReceiptQuery>;
119
+
113
120
  export const AgentEvent = z.object({
114
121
  type: z.literal("agent.event"),
115
122
  text: z.string(),
@@ -21,6 +21,7 @@ import { CapabilityUsageStatus } from "./messages/capabilities";
21
21
  import {
22
22
  AgentEvent,
23
23
  DeliveryReceipt,
24
+ DeliveryReceiptQuery,
24
25
  MAX_ATTACHMENT_BASE64_CHARS,
25
26
  SessionEventsRequest,
26
27
  SessionsRefresh,
@@ -130,6 +131,7 @@ export const Payload = z.union([
130
131
  HostGrant,
131
132
  UserAttachmentUpload,
132
133
  DeliveryReceipt,
134
+ DeliveryReceiptQuery,
133
135
  AgentEvent,
134
136
  SessionSubscription,
135
137
  SessionEventsRequest,