granttap-mcp 0.8.97 → 0.8.98
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/apps/bridge/src/delivery/index.ts +19 -1
- package/apps/bridge/src/monitor/handlers/receipts.ts +2 -0
- package/apps/bridge/src/monitor/handlers/user-message.ts +2 -0
- package/apps/bridge/src/monitor/message-delivery/README.md +4 -0
- package/apps/bridge/src/monitor/message-delivery/activity.ts +14 -0
- package/apps/bridge/src/monitor/message-delivery/index.ts +4 -2
- package/apps/bridge/src/monitor/message-delivery/status-query.ts +1 -1
- package/apps/bridge/src/reply/index.ts +11 -4
- package/apps/bridge/src/reply/payload/types.ts +2 -0
- package/apps/bridge/src/reply/process/index.ts +13 -1
- package/apps/bridge/src/reply/process/provider-headless.ts +4 -2
- package/apps/bridge/src/task-transfer/README.md +4 -0
- package/apps/bridge/src/task-transfer/index.ts +12 -2
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/core/outbound-scheduler/index.ts +3 -3
- package/packages/core/relay-client.ts +3 -2
- package/packages/core/socket-lifecycle/README.md +4 -0
- package/packages/protocol/messages/interaction/index.ts +1 -0
|
@@ -12,6 +12,7 @@ type DeliveryRecord = {
|
|
|
12
12
|
state: "processing" | "completed";
|
|
13
13
|
updatedAt: number;
|
|
14
14
|
processingAcknowledged?: boolean;
|
|
15
|
+
agentActivityAt?: number;
|
|
15
16
|
rejectedError?: string;
|
|
16
17
|
scope?: DeliveryScope;
|
|
17
18
|
};
|
|
@@ -69,6 +70,22 @@ export function acknowledgeProcessingDelivery(messageId: string, now = Date.now(
|
|
|
69
70
|
save(records, now);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/** Provider output, rather than process creation, confirms the original operation started. */
|
|
74
|
+
export function acknowledgeAgentActivity(messageId: string, at = Date.now(), scope?: DeliveryScope): number | undefined {
|
|
75
|
+
const records = load();
|
|
76
|
+
const record = records[messageId] ?? (scope
|
|
77
|
+
? { state: 'completed' as const, updatedAt: at, processingAcknowledged: true, scope } : undefined);
|
|
78
|
+
if (!record || record.rejectedError || !Number.isFinite(at) || at <= 0
|
|
79
|
+
|| records[messageId] && scope && !record.scope
|
|
80
|
+
|| scope && record.scope && (scope.room !== record.scope.room
|
|
81
|
+
|| scope.peerPublicKey !== record.scope.peerPublicKey || scope.sessionId !== record.scope.sessionId)) return undefined;
|
|
82
|
+
if (record.agentActivityAt == null) {
|
|
83
|
+
records[messageId] = { ...record, agentActivityAt: at };
|
|
84
|
+
save(records, Date.now());
|
|
85
|
+
}
|
|
86
|
+
return records[messageId]?.agentActivityAt;
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
export function processingDeliveryAcknowledged(messageId: string): boolean {
|
|
73
90
|
const record = load()[messageId];
|
|
74
91
|
return record?.state === "processing" && record.processingAcknowledged === true;
|
|
@@ -114,7 +131,8 @@ export function deliveryReceiptStatus(messageId: string, scope: DeliveryScope, n
|
|
|
114
131
|
if (!saved || saved.room !== scope.room || saved.peerPublicKey !== scope.peerPublicKey
|
|
115
132
|
|| saved.sessionId !== scope.sessionId || record.updatedAt < now - MAX_AGE_MS
|
|
116
133
|
|| record.updatedAt > now || record.state === 'processing' && !record.processingAcknowledged) return undefined;
|
|
117
|
-
return { status: record.rejectedError ? 'rejected' as const : 'accepted' as const,
|
|
134
|
+
return { status: record.rejectedError ? 'rejected' as const : 'accepted' as const,
|
|
135
|
+
error: record.rejectedError, agentActivityAt: record.agentActivityAt };
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
/** Release a failed attempt so a relay retry can safely try again. */
|
|
@@ -12,6 +12,7 @@ export async function sendDeliveryReceipt(
|
|
|
12
12
|
error?: string,
|
|
13
13
|
sessionId?: string,
|
|
14
14
|
peerPublicKey?: string,
|
|
15
|
+
agentActivityAt?: number,
|
|
15
16
|
): Promise<void> {
|
|
16
17
|
const payload = {
|
|
17
18
|
type: "delivery.receipt" as const,
|
|
@@ -20,6 +21,7 @@ export async function sendDeliveryReceipt(
|
|
|
20
21
|
status,
|
|
21
22
|
error,
|
|
22
23
|
receivedAt: Date.now(),
|
|
24
|
+
...(agentActivityAt ? { agentActivityAt } : {}),
|
|
23
25
|
};
|
|
24
26
|
const options = { ttlMs: 24 * 60 * 60_000, peerPublicKey };
|
|
25
27
|
// A controller receipt is control-plane state. It remains device-encrypted,
|
|
@@ -21,6 +21,7 @@ import { publishedSession } from "../../session-catalog";
|
|
|
21
21
|
import { localTaskOrigins, type TaskOriginRegistry, type TaskOriginScope } from '../../task-origins';
|
|
22
22
|
import { agentEventForUserMessage, sendDeliveryReceipt } from "./receipts";
|
|
23
23
|
import { resolveMessageAttachments } from "./receipts";
|
|
24
|
+
import { reportMessageAgentActivity } from '../message-delivery/activity';
|
|
24
25
|
import { acceptWhenStarted, PHONE_RUN_LIMIT_MS } from "../message-delivery/acceptance";
|
|
25
26
|
|
|
26
27
|
async function startNewAgentTask(input: {
|
|
@@ -172,6 +173,7 @@ export async function handleUserMessage(
|
|
|
172
173
|
const startedAt = Date.now();
|
|
173
174
|
const run = deliverToSession(target, message.text, PHONE_RUN_LIMIT_MS, attachments, {
|
|
174
175
|
messageId: message.messageId,
|
|
176
|
+
onAgentActivity: () => reportMessageAgentActivity(client, message, peerPublicKey),
|
|
175
177
|
preferredMcp: message.preferredMcp,
|
|
176
178
|
skill: message.skill,
|
|
177
179
|
model: message.model,
|
|
@@ -22,3 +22,7 @@ message again. Only the original authenticated controller, room, and session
|
|
|
22
22
|
may read it. Older unscoped records are not exposed through this query.
|
|
23
23
|
Receipts to a known controller use its device encryption directly, so a missed
|
|
24
24
|
task-key grant cannot hide a provider rejection or processing acknowledgement.
|
|
25
|
+
|
|
26
|
+
A correlated `agentActivityAt` receipt is emitted only after provider stream output
|
|
27
|
+
confirms activity. It is retained under the original controller, room and session
|
|
28
|
+
scope, survives receipt refresh, and does not claim a final response.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RelayClient } from '../../../../../packages/core/relay-client';
|
|
2
|
+
import type { UserMessage } from '../../../../../packages/protocol/schema';
|
|
3
|
+
import { acknowledgeAgentActivity } from '../../delivery';
|
|
4
|
+
import { sendDeliveryReceipt } from '../handlers/receipts';
|
|
5
|
+
|
|
6
|
+
/** The provider stream confirms activity for one authenticated original operation. */
|
|
7
|
+
export function reportMessageAgentActivity(client: RelayClient, message: UserMessage, peerPublicKey?: string): void {
|
|
8
|
+
if (!message.messageId || !message.sessionId || !peerPublicKey) return;
|
|
9
|
+
const at = acknowledgeAgentActivity(message.messageId, Date.now(), {
|
|
10
|
+
room: client.room, sessionId: message.sessionId, peerPublicKey,
|
|
11
|
+
});
|
|
12
|
+
if (at) void sendDeliveryReceipt(client, message.messageId, 'accepted', undefined,
|
|
13
|
+
message.sessionId, peerPublicKey, at);
|
|
14
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Payload } from "../../../../../packages/protocol/schema";
|
|
2
2
|
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
3
3
|
import { abandonDelivery, beginDelivery, completeDelivery,
|
|
4
|
-
acknowledgeProcessingDelivery, processingDeliveryAcknowledged, completedDeliveryFailure } from "../../delivery";
|
|
4
|
+
acknowledgeProcessingDelivery, processingDeliveryAcknowledged, completedDeliveryFailure, deliveryReceiptStatus } from "../../delivery";
|
|
5
5
|
import { sendDeliveryReceipt } from "../handlers/receipts";
|
|
6
6
|
import { handleUserMessage } from "../handlers/user-message";
|
|
7
7
|
|
|
@@ -20,7 +20,9 @@ export async function handleInboundUserMessage(
|
|
|
20
20
|
if (state === "completed" || processingDeliveryAcknowledged(payload.messageId)) {
|
|
21
21
|
const failure = completedDeliveryFailure(payload.messageId);
|
|
22
22
|
await sendDeliveryReceipt(client, payload.messageId, failure ? 'rejected' : 'accepted',
|
|
23
|
-
failure, payload.sessionId, peerPublicKey
|
|
23
|
+
failure, payload.sessionId, peerPublicKey, peerPublicKey && payload.sessionId
|
|
24
|
+
? deliveryReceiptStatus(payload.messageId, { room: client.room, peerPublicKey,
|
|
25
|
+
sessionId: payload.sessionId })?.agentActivityAt : undefined);
|
|
24
26
|
}
|
|
25
27
|
return true;
|
|
26
28
|
}
|
|
@@ -7,6 +7,6 @@ export async function handleDeliveryReceiptQuery(client: RelayClient, query: Del
|
|
|
7
7
|
peerPublicKey?: string): Promise<boolean> {
|
|
8
8
|
if (!peerPublicKey) return true;
|
|
9
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);
|
|
10
|
+
if (receipt) await sendDeliveryReceipt(client, query.messageId, receipt.status, receipt.error, query.sessionId, peerPublicKey, receipt.agentActivityAt);
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
@@ -65,9 +65,15 @@ export function deliverToSession(
|
|
|
65
65
|
if (/^transfer-[a-f0-9]{48}$/.test(session.sessionId)) {
|
|
66
66
|
return queued(session.sessionId, async () => (await localTaskTransfers().deliver(
|
|
67
67
|
session.sessionId, text, attachments, options, {
|
|
68
|
-
start: request =>
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
start: request => request.provider === "grok"
|
|
69
|
+
? createGrokSession(request.text, request.cwd, timeoutMs, request.attachments,
|
|
70
|
+
request.model, request.operationKey, request.effort, nativeId => {
|
|
71
|
+
request.onNativeSession(nativeId);
|
|
72
|
+
options.onAgentActivity?.(nativeId);
|
|
73
|
+
})
|
|
74
|
+
: ({ claude: createClaudeSession, codex: createCodexSession,
|
|
75
|
+
cursor: createCursorSession }[request.provider])(request.text, request.cwd,
|
|
76
|
+
timeoutMs, request.attachments, request.model, request.operationKey, request.effort),
|
|
71
77
|
resume: (native, body, images, settings) => withAttachments(images, body, prepared => runDelivery(native,
|
|
72
78
|
native.agent === "codex" ? prepared.prompt : prepared.claudePrompt, timeoutMs, prepared.images, settings)),
|
|
73
79
|
},
|
|
@@ -139,10 +145,11 @@ export function createGrokSession(
|
|
|
139
145
|
model?: string,
|
|
140
146
|
operationKey?: string,
|
|
141
147
|
effort?: string,
|
|
148
|
+
onNativeSession?: (sessionId: string) => void,
|
|
142
149
|
): Promise<ReplyResult> {
|
|
143
150
|
const workspace = resolveAgentWorkspace(cwd, "grok");
|
|
144
151
|
return queued("__new_grok_task__", () => withAttachments(attachments, text, (prepared) =>
|
|
145
|
-
runGrokNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey, effort),
|
|
152
|
+
runGrokNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey, effort, onNativeSession),
|
|
146
153
|
));
|
|
147
154
|
}
|
|
148
155
|
|
|
@@ -3,6 +3,8 @@ export type ReplyResult =
|
|
|
3
3
|
| { ok: false; error: string; delivery?: 'native' };
|
|
4
4
|
|
|
5
5
|
export type DeliveryOptions = {
|
|
6
|
+
/** Called only after provider output confirms this operation has started. */
|
|
7
|
+
onAgentActivity?: (nativeSessionId: string) => void;
|
|
6
8
|
/** Stable controller delivery ID, also used by the provider's native admission. */
|
|
7
9
|
messageId?: string;
|
|
8
10
|
/** Computer-generated identity before temporary attachment paths are inserted. */
|
|
@@ -74,6 +74,7 @@ export function runningProcessCount(key: string): number {
|
|
|
74
74
|
|
|
75
75
|
export type ProviderAdmission = {
|
|
76
76
|
confirmed: (stdout: string) => boolean;
|
|
77
|
+
onConfirmed?: () => void;
|
|
77
78
|
withinMs: number;
|
|
78
79
|
timeoutMessage: string;
|
|
79
80
|
};
|
|
@@ -107,6 +108,7 @@ export function runProcess(
|
|
|
107
108
|
let stdout = "";
|
|
108
109
|
let stderr = "";
|
|
109
110
|
let done = false;
|
|
111
|
+
let admissionReported = false;
|
|
110
112
|
const finish = (result: ReplyResult): void => {
|
|
111
113
|
if (done) return;
|
|
112
114
|
done = true;
|
|
@@ -125,7 +127,17 @@ export function runProcess(
|
|
|
125
127
|
signalOwnedProcess(child, "SIGKILL");
|
|
126
128
|
finish({ ok: false, error: admission.timeoutMessage });
|
|
127
129
|
}, admission.withinMs) : undefined;
|
|
128
|
-
child.stdout?.on("data", (chunk) => {
|
|
130
|
+
child.stdout?.on("data", (chunk) => {
|
|
131
|
+
stdout += chunk;
|
|
132
|
+
if (done || admissionReported || !admission?.confirmed(stdout)) return;
|
|
133
|
+
admissionReported = true;
|
|
134
|
+
clearTimeout(startupTimer);
|
|
135
|
+
try { admission.onConfirmed?.(); }
|
|
136
|
+
catch {
|
|
137
|
+
signalOwnedProcess(child, "SIGTERM");
|
|
138
|
+
finish({ ok: false, error: "Provider activity could not be recorded safely.", delivery: "native" });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
129
141
|
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
130
142
|
child.on("error", (error) => {
|
|
131
143
|
finish({ ok: false, error: `${command}: ${error.message}` });
|
|
@@ -29,6 +29,7 @@ export async function runCursorResume(
|
|
|
29
29
|
|
|
30
30
|
export function runGrokNew(
|
|
31
31
|
text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string, effort?: string,
|
|
32
|
+
onNativeSession?: (sessionId: string) => void,
|
|
32
33
|
): Promise<ReplyResult> {
|
|
33
34
|
const sessionId = randomUUID();
|
|
34
35
|
const args = [
|
|
@@ -37,7 +38,7 @@ export function runGrokNew(
|
|
|
37
38
|
"-p", text, "--output-format", "streaming-json",
|
|
38
39
|
];
|
|
39
40
|
return runProcess(grokBin(), args, cwd, timeoutMs, (stdout) => parseGrok(stdout, sessionId),
|
|
40
|
-
undefined, operationKey, grokAdmission);
|
|
41
|
+
undefined, operationKey, { ...grokAdmission, onConfirmed: () => onNativeSession?.(sessionId) });
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function runGrokResume(
|
|
@@ -58,6 +59,7 @@ export function runGrokResume(
|
|
|
58
59
|
return runProcess(
|
|
59
60
|
grokBin(), args, session.cwd, timeoutMs,
|
|
60
61
|
(stdout) => parseGrok(stdout, session.sessionId),
|
|
61
|
-
undefined, session.sessionId, grokAdmission,
|
|
62
|
+
undefined, session.sessionId, { ...grokAdmission,
|
|
63
|
+
onConfirmed: () => options.onAgentActivity?.(session.sessionId) },
|
|
62
64
|
);
|
|
63
65
|
}
|
|
@@ -21,3 +21,7 @@ Tests: `tests/prepared.test.ts` covers preparation, restart, model/effort forwar
|
|
|
21
21
|
context persistence, stable catalog identity and ambiguous first submissions.
|
|
22
22
|
|
|
23
23
|
Licensed under the GrantTap Commercial Source License. See LICENSE and NOTICE.
|
|
24
|
+
|
|
25
|
+
The first Grok turn binds its confirmed native session before the final answer,
|
|
26
|
+
so the transferred Task can publish live native history while the turn runs.
|
|
27
|
+
The durable launch remains in progress and cannot start another execution.
|
|
@@ -7,7 +7,7 @@ export type { PreparedTransfer, TransferPreparation } from './records';
|
|
|
7
7
|
|
|
8
8
|
type TransferProvider = {
|
|
9
9
|
start: (request: { provider: CodingAgent; text: string; cwd: string; attachments: UserAttachment[];
|
|
10
|
-
model?: string; effort?: string; operationKey: string }) => Promise<ReplyResult>;
|
|
10
|
+
model?: string; effort?: string; operationKey: string; onNativeSession: (sessionId: string) => void }) => Promise<ReplyResult>;
|
|
11
11
|
resume: (session: SessionInfo, text: string, attachments: UserAttachment[], options: DeliveryOptions) => Promise<ReplyResult>;
|
|
12
12
|
};
|
|
13
13
|
const interrupted = (): ReplyResult => ({ ok: false,
|
|
@@ -68,7 +68,17 @@ export class PreparedTaskTransfers extends TransferPersistence {
|
|
|
68
68
|
ownsLaunch = true;
|
|
69
69
|
const result = await provider.start({ provider: row.capsule.targetProvider as CodingAgent,
|
|
70
70
|
text: this.prompt(row, context, text), cwd: row.workspace, attachments,
|
|
71
|
-
model: options.model ?? row.capsule.targetModel, effort: options.effort, operationKey: id
|
|
71
|
+
model: options.model ?? row.capsule.targetModel, effort: options.effort, operationKey: id,
|
|
72
|
+
onNativeSession: nativeId => {
|
|
73
|
+
if (!/^[a-zA-Z0-9_-]{1,256}$/.test(nativeId)) throw new Error('Invalid native session identity');
|
|
74
|
+
this.update(id, current => {
|
|
75
|
+
if (current.phase !== 'starting' || current.ownerPid !== process.pid
|
|
76
|
+
|| current.nativeSessionId && current.nativeSessionId !== nativeId) {
|
|
77
|
+
throw new Error('The transfer no longer owns this provider launch');
|
|
78
|
+
}
|
|
79
|
+
return { ...current, nativeSessionId: nativeId, updatedAt: Date.now() };
|
|
80
|
+
});
|
|
81
|
+
} });
|
|
72
82
|
if (!result.ok || !result.sessionId) {
|
|
73
83
|
this.update(id, current => ({ ...current, phase: 'uncertain', updatedAt: Date.now() }));
|
|
74
84
|
return result.ok ? interrupted() : result;
|
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.98
|
|
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.98\";\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.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.98";
|
|
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
|
@@ -71,9 +71,9 @@ export class OutboundFrameQueue {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
const
|
|
74
|
+
export const priorityControlPayload = (type: Payload["type"]) => type === "hello" || type.startsWith("approval.")
|
|
75
75
|
|| type.startsWith("config.") || type.startsWith("pairing.") || type === "session.key.grant"
|
|
76
|
-
|| type === "sessions.status" || type === "machine.heartbeat" || type === "approvals.status";
|
|
76
|
+
|| type === "delivery.receipt" || type === "sessions.status" || type === "machine.heartbeat" || type === "approvals.status";
|
|
77
77
|
|
|
78
78
|
export async function sendEncryptedPayload(queue: OutboundFrameQueue, config: PeerConfig,
|
|
79
79
|
allowed: (key: string) => boolean,
|
|
@@ -83,6 +83,6 @@ export async function sendEncryptedPayload(queue: OutboundFrameQueue, config: Pe
|
|
|
83
83
|
if (!peers.length) throw new Error("no authorized pairing peers");
|
|
84
84
|
for (const peerPublicKey of peers) {
|
|
85
85
|
await queue.send(sealEnvelope({ ...config, peerPublicKey }, payload, to,
|
|
86
|
-
peers.length === 1 ? options : { ...options, deliveryId: undefined }),
|
|
86
|
+
peers.length === 1 ? options : { ...options, deliveryId: undefined }), priorityControlPayload(payload.type) || options.wake === true);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -2,7 +2,7 @@ import { retainedPayloadOptions } from "./retained-state";
|
|
|
2
2
|
/** Shared authenticated relay transport. See README.md for delivery and encryption boundaries. */
|
|
3
3
|
import WebSocket from "ws";
|
|
4
4
|
import { ManagedRelayLease, RelaySocketHealth, RelayReconnectBackoff, RelayConnectionNotifications, relayUpgradeHeaders } from "./socket-lifecycle";
|
|
5
|
-
import { OutboundFrameQueue, sendEncryptedPayload } from "./outbound-scheduler";
|
|
5
|
+
import { OutboundFrameQueue, sendEncryptedPayload, priorityControlPayload } from "./outbound-scheduler";
|
|
6
6
|
import { openRelayMessage, parseRelayControl, waitForRelayPayload } from "./relay-inbound";
|
|
7
7
|
import { consumeTaskJobOffer, type TaskDispatchListener } from "./task-dispatch";
|
|
8
8
|
import { Payload, type Role } from "../protocol/schema";
|
|
@@ -213,7 +213,8 @@ export class RelayClient {
|
|
|
213
213
|
if (!peerPublicKeys(this.cfg.peerPublicKey, this.cfg.extraPeerPublicKeys).includes(peerPublicKey)
|
|
214
214
|
|| !this.peerIsAllowed(peerPublicKey)) throw new Error("controller peer is not authorized");
|
|
215
215
|
if (!this.frames) throw new Error("relay not connected");
|
|
216
|
-
await this.frames.send(sealEnvelope({ ...this.cfg, peerPublicKey }, payload, this.otherRole(), { ...retainedPayloadOptions(payload), ...options, retainedState: this.retainedState, recipientRouting: this.recipientRouting }),
|
|
216
|
+
await this.frames.send(sealEnvelope({ ...this.cfg, peerPublicKey }, payload, this.otherRole(), { ...retainedPayloadOptions(payload), ...options, retainedState: this.retainedState, recipientRouting: this.recipientRouting }),
|
|
217
|
+
priorityControlPayload(payload.type) || options.wake === true);
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
setSessionKey(sessionId: string, key: string): void {
|
|
@@ -21,3 +21,7 @@ Consumers can immediately replace stale snapshots; notification failures cannot
|
|
|
21
21
|
interrupt the transport, and unsubscribing prevents later callbacks.
|
|
22
22
|
|
|
23
23
|
Licensed under the GrantTap Commercial Source License in the repository root.
|
|
24
|
+
|
|
25
|
+
Controller-addressed history uses the snapshot queue. Delivery receipts and
|
|
26
|
+
heartbeats use the control queue, so a history download cannot put them behind
|
|
27
|
+
its pending frames. An in-flight WebSocket frame is allowed to finish.
|
|
@@ -107,6 +107,7 @@ export const DeliveryReceipt = z.object({
|
|
|
107
107
|
status: z.enum(["accepted", "rejected"]),
|
|
108
108
|
error: z.string().max(500).optional(),
|
|
109
109
|
receivedAt: z.number(),
|
|
110
|
+
agentActivityAt: z.number().finite().positive().optional(),
|
|
110
111
|
});
|
|
111
112
|
export type DeliveryReceipt = z.infer<typeof DeliveryReceipt>;
|
|
112
113
|
|