granttap-mcp 0.8.113 → 0.8.115

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.
@@ -48,8 +48,12 @@ explicitly unavailable.
48
48
 
49
49
  `invocation-ingest.ts` reads bounded complete transcript lines and exact-call
50
50
  hook denial metadata without forwarding tool arguments, output, or reason text.
51
- Cursor decisions without a native call ID stay unattributed. Events are replayed
52
- with stable IDs after an IPC failure. `invocation-query.ts` returns bounded
51
+ Cursor decisions without a native call ID stay unattributed. Unconfirmed events retain their exact IDs and timestamps across IPC retries;
52
+ confirmed prefixes are not resent. Transient failures back off up to one minute,
53
+ while journal-capacity refusals wait five minutes. Source cursors advance only
54
+ after the retained batch is confirmed; pending sources are bounded and deferred
55
+ source files remain unread. This neither clears history nor increases the Engine
56
+ journal limit. `invocation-query.ts` returns bounded
53
57
  Engine history to the phone under the Project key, while
54
58
  `invocation-scope.ts` exposes one Task slice through the caller's existing
55
59
  opaque Mesh capability. The Engine, not this bridge or Usage telemetry, owns
@@ -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.5", platform: "darwin", arch: "arm64",
12
- sha256: "bd16babf11fd3d7e65b9713797b977deba1670ba41230a0e6607ce938b847455", bytes: 12202080 },
11
+ { version: "0.1.6", platform: "darwin", arch: "arm64",
12
+ sha256: "71819584e4a5edb064bcc23173edfc13ed3dcd61aa4487ba3febc9eb10708bb8", bytes: 12201872 },
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,11 @@
1
+ # Invocation delivery recovery
2
+
3
+ Public entry: `index.ts`. Retains the unconfirmed suffix of a bounded transcript
4
+ batch across IPC failures, preserving exact event identity and source cursors.
5
+ Transient errors use exponential backoff; a full journal waits five minutes.
6
+ No durable history is deleted or silently replaced to recover capacity.
7
+
8
+ Behavior is exercised through the ingestor by
9
+ `tests/invocation/recovery/ingest.test.ts`.
10
+
11
+ License: GrantTap Commercial Source License in the repository-root `LICENSE`.
@@ -0,0 +1,53 @@
1
+ import { EngineRemoteError, type EngineOperation, type EngineResult } from '../../protocol/engine-protocol';
2
+ import type { InvocationEvent } from '../../protocol/engine-invocation-protocol';
3
+ import type { InvocationLogCursor } from '../log-reader';
4
+ import type { InvocationParseState } from '../parse';
5
+
6
+ export type InvocationBatch = {
7
+ events: InvocationEvent[];
8
+ nextEvent: number;
9
+ next: InvocationLogCursor;
10
+ parse?: InvocationParseState;
11
+ };
12
+
13
+ const MAX_PENDING_SOURCES = 32;
14
+ const CAPACITY_RETRY_MS = 5 * 60_000;
15
+ type Client = { request(operation: EngineOperation): Promise<EngineResult> };
16
+
17
+ /** Retain uncertain evidence, not a replay of every successfully committed event. */
18
+ export class InvocationDelivery {
19
+ private pending = new Map<string, InvocationBatch>();
20
+ private retryAt = 0;
21
+ private failures = 0;
22
+
23
+ constructor(private client: Client, private now: () => number = Date.now) {}
24
+
25
+ ready(): boolean { return this.now() >= this.retryAt; }
26
+
27
+ async advance(key: string, prepare: () => InvocationBatch) {
28
+ if (!this.pending.has(key) && this.pending.size >= MAX_PENDING_SOURCES) return undefined;
29
+ let batch = this.pending.get(key);
30
+ if (!batch) {
31
+ try { batch = prepare(); }
32
+ catch { return undefined; } // An unavailable source is not a committed cursor.
33
+ }
34
+ let accepted = 0;
35
+ for (; batch.nextEvent < batch.events.length; batch.nextEvent++) {
36
+ try {
37
+ await this.client.request({ operation: 'invocation.observe', input: batch.events[batch.nextEvent]! });
38
+ accepted++;
39
+ } catch (error) {
40
+ this.pending.set(key, batch);
41
+ this.failures++;
42
+ const capacity = error instanceof EngineRemoteError && (error.code === 'INVOCATION_CAPACITY'
43
+ || (error.code === 'INVOCATION_STATE_WRITE_FAILED' && /size limit/i.test(error.message)));
44
+ this.retryAt = this.now() + (capacity ? CAPACITY_RETRY_MS
45
+ : Math.min(60_000, 1_000 * 2 ** Math.min(this.failures - 1, 6)));
46
+ return { batch, accepted, complete: false };
47
+ }
48
+ }
49
+ this.pending.delete(key);
50
+ if (accepted) { this.failures = 0; this.retryAt = 0; }
51
+ return { batch, accepted, complete: true };
52
+ }
53
+ }
@@ -17,6 +17,7 @@ import {
17
17
  } from "./parse";
18
18
  import type { InvocationEvent } from "../protocol/engine-invocation-protocol";
19
19
  import { parseInvocationDecision } from "./decision-parse";
20
+ import { InvocationDelivery, type InvocationBatch } from './delivery';
20
21
 
21
22
  type Provider = "claude" | "codex" | "cursor";
22
23
  type SourceState = { cursor: InvocationLogCursor; parse: InvocationParseState };
@@ -29,6 +30,7 @@ export type InvocationIngestOptions = {
29
30
  paths: (session: SessionInfo) => string[];
30
31
  inspect: (cwd: string) => RepositoryFacts;
31
32
  decisionPath?: (session: SessionInfo) => string;
33
+ now?: () => number;
32
34
  };
33
35
 
34
36
  function transcriptPaths(session: SessionInfo): string[] {
@@ -41,9 +43,10 @@ function transcriptPaths(session: SessionInfo): string[] {
41
43
  export function createInvocationIngestor(options: InvocationIngestOptions) {
42
44
  const states = new Map<string, SourceState>();
43
45
  const decisionStates = new Map<string, InvocationLogCursor>();
46
+ const delivery = new InvocationDelivery(options.client, options.now);
44
47
  return {
45
48
  async ingest(sessions: readonly SessionInfo[]): Promise<number> {
46
- if (!options.enabled()) return 0;
49
+ if (!options.enabled() || !delivery.ready()) return 0;
47
50
  const candidates = sessions.filter((session) => session.projectId && session.taskId
48
51
  && ["claude", "codex", "cursor"].includes(session.agent));
49
52
  if (candidates.length === 0) return 0;
@@ -54,68 +57,28 @@ export function createInvocationIngestor(options: InvocationIngestOptions) {
54
57
  const provider = session.agent as Provider;
55
58
  const facts = session.cwd ? options.inspect(session.cwd) : undefined;
56
59
  for (const path of options.paths(session)) {
57
- const key = `${provider}\0${session.sessionId}\0${path}`;
60
+ const key = `transcript\0${provider}\0${session.sessionId}\0${path}`;
58
61
  const known = states.get(key);
59
- let batch;
60
- try { batch = readInvocationBatch(path, known?.cursor); }
61
- catch { continue; }
62
- const parse: InvocationParseState = {
63
- pending: new Map(known?.parse.pending ?? []),
64
- };
65
- const records = [
66
- ...batch.gaps.map((offset) => ({ offset, gap: true as const })),
67
- ...batch.lines.map((row) => ({ ...row, gap: false as const })),
68
- ].sort((a, b) => a.offset - b.offset || Number(b.gap) - Number(a.gap));
69
62
  const thread = hash([path]);
70
- const events: InvocationEvent[] = [];
71
- for (const record of records) {
72
- if (record.gap) parse.pending.clear();
73
- const observed = record.gap
74
- ? [sourceGapFact(record.offset)]
75
- : parseInvocationLine(provider, record.line, parse, record.offset);
76
- for (const fact of observed) {
77
- events.push(eventFor({
78
- session, provider, computer: options.computer(), thread,
79
- offset: record.offset, fact, repository: facts,
80
- }));
81
- }
82
- }
83
- try {
84
- for (const event of events) {
85
- await options.client.request({ operation: "invocation.observe", input: event });
86
- accepted += 1;
87
- }
88
- } catch {
89
- // No cursor advance: the next pass replays this batch under stable
90
- // event IDs. Engine deduplicates already committed evidence.
91
- return accepted;
92
- }
93
- states.set(key, { cursor: batch.next, parse });
63
+ const result = await delivery.advance(key, () => transcriptBatch(path, known, provider,
64
+ (fact, offset) => eventFor({ session, provider, computer: options.computer(), thread,
65
+ offset, fact, repository: facts })));
66
+ if (!result) continue;
67
+ accepted += result.accepted;
68
+ if (!result.complete) return accepted;
69
+ states.set(key, { cursor: result.batch.next, parse: result.batch.parse! });
94
70
  }
95
71
  const decisionPath = options.decisionPath?.(session);
96
72
  if (decisionPath) {
97
- const key = `${provider}\0${session.sessionId}\0${decisionPath}`;
98
- let batch;
99
- try { batch = readInvocationBatch(decisionPath, decisionStates.get(key)); }
100
- catch { continue; }
73
+ const key = `decision\0${provider}\0${session.sessionId}\0${decisionPath}`;
101
74
  const thread = hash([decisionPath]);
102
- const events = [
103
- ...batch.gaps.map((offset) => ({ offset, gap: true as const })),
104
- ...batch.lines.map((row) => ({ ...row, gap: false as const })),
105
- ].sort((a, b) => a.offset - b.offset).flatMap((record) =>
106
- (record.gap ? [sourceGapFact(record.offset)]
107
- : parseInvocationDecision(record.line, provider, record.offset))
108
- .map((fact) => eventFor({
109
- session, provider, computer: options.computer(), thread,
110
- offset: record.offset, fact, repository: facts,
111
- })));
112
- try {
113
- for (const event of events) {
114
- await options.client.request({ operation: "invocation.observe", input: event });
115
- accepted += 1;
116
- }
117
- } catch { return accepted; }
118
- decisionStates.set(key, batch.next);
75
+ const result = await delivery.advance(key, () => decisionBatch(decisionPath, decisionStates.get(key), provider,
76
+ (fact, offset) => eventFor({ session, provider, computer: options.computer(), thread,
77
+ offset, fact, repository: facts })));
78
+ if (!result) continue;
79
+ accepted += result.accepted;
80
+ if (!result.complete) return accepted;
81
+ decisionStates.set(key, result.batch.next);
119
82
  }
120
83
  }
121
84
  return accepted;
@@ -123,6 +86,36 @@ export function createInvocationIngestor(options: InvocationIngestOptions) {
123
86
  };
124
87
  }
125
88
 
89
+ type EventBuilder = (fact: InvocationFact, offset: number) => InvocationEvent;
90
+
91
+ function transcriptBatch(path: string, known: SourceState | undefined, provider: Provider, event: EventBuilder): InvocationBatch {
92
+ const batch = readInvocationBatch(path, known?.cursor);
93
+ const parse: InvocationParseState = { pending: new Map(known?.parse.pending ?? []) };
94
+ const records = [
95
+ ...batch.gaps.map(offset => ({ offset, gap: true as const })),
96
+ ...batch.lines.map(row => ({ ...row, gap: false as const })),
97
+ ].sort((a, b) => a.offset - b.offset || Number(b.gap) - Number(a.gap));
98
+ const events: InvocationEvent[] = [];
99
+ for (const record of records) {
100
+ if (record.gap) parse.pending.clear();
101
+ const observed = record.gap ? [sourceGapFact(record.offset)]
102
+ : parseInvocationLine(provider, record.line, parse, record.offset);
103
+ for (const fact of observed) events.push(event(fact, record.offset));
104
+ }
105
+ return { events, nextEvent: 0, next: batch.next, parse };
106
+ }
107
+
108
+ function decisionBatch(path: string, cursor: InvocationLogCursor | undefined, provider: Provider, event: EventBuilder): InvocationBatch {
109
+ const batch = readInvocationBatch(path, cursor);
110
+ const records = [
111
+ ...batch.gaps.map(offset => ({ offset, gap: true as const })),
112
+ ...batch.lines.map(row => ({ ...row, gap: false as const })),
113
+ ].sort((a, b) => a.offset - b.offset);
114
+ const events = records.flatMap(record => (record.gap ? [sourceGapFact(record.offset)]
115
+ : parseInvocationDecision(record.line, provider, record.offset)).map(fact => event(fact, record.offset)));
116
+ return { events, nextEvent: 0, next: batch.next };
117
+ }
118
+
126
119
  function eventFor(input: {
127
120
  session: SessionInfo;
128
121
  provider: Provider;
@@ -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.113
13
+ npm install -g granttap-mcp@0.8.115
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.113\";\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.115\";\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.113";
10
+ const PACKAGE = "granttap-mcp@0.8.115";
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.113",
3
+ "version": "0.8.115",
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": {