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.
@@ -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, error: record.rejectedError };
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 => ({ claude: createClaudeSession, codex: createCodexSession,
69
- cursor: createCursorSession, grok: createGrokSession }[request.provider])(
70
- request.text, request.cwd, timeoutMs, request.attachments, request.model, request.operationKey, request.effort),
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) => { stdout += 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;
@@ -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.97
13
+ npm install -g granttap-mcp@0.8.98
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.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"
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.97";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.97",
3
+ "version": "0.8.98",
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": {
@@ -71,9 +71,9 @@ export class OutboundFrameQueue {
71
71
  }
72
72
  }
73
73
 
74
- const control = (type: Payload["type"]) => type === "hello" || type.startsWith("approval.")
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 }), control(payload.type) || options.wake === true);
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 }), true);
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