@pcircle/memesh 4.7.3 → 4.8.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +3 -1
- package/README.de.md +42 -16
- package/README.md +46 -19
- package/README.zh-TW.md +42 -17
- package/dashboard/dist/index.html +1 -1
- package/dist/core/agent-message-storage.d.ts +76 -0
- package/dist/core/agent-message-storage.d.ts.map +1 -0
- package/dist/core/agent-message-storage.js +359 -0
- package/dist/core/agent-message-storage.js.map +1 -0
- package/dist/core/agent-messaging.d.ts +224 -0
- package/dist/core/agent-messaging.d.ts.map +1 -0
- package/dist/core/agent-messaging.js +881 -0
- package/dist/core/agent-messaging.js.map +1 -0
- package/dist/core/agent-router.d.ts +194 -0
- package/dist/core/agent-router.d.ts.map +1 -0
- package/dist/core/agent-router.js +1138 -0
- package/dist/core/agent-router.js.map +1 -0
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +8 -1
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/doctor.d.ts +19 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +176 -2
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +8 -5
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +88 -3
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/product-improvements.d.ts +61 -0
- package/dist/core/product-improvements.d.ts.map +1 -0
- package/dist/core/product-improvements.js +216 -0
- package/dist/core/product-improvements.js.map +1 -0
- package/dist/core/repo-state.d.ts +11 -0
- package/dist/core/repo-state.d.ts.map +1 -0
- package/dist/core/repo-state.js +70 -0
- package/dist/core/repo-state.js.map +1 -0
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +70 -0
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/task-state.d.ts.map +1 -1
- package/dist/core/task-state.js +3 -3
- package/dist/core/task-state.js.map +1 -1
- package/dist/core/updater.d.ts +2 -0
- package/dist/core/updater.d.ts.map +1 -1
- package/dist/core/updater.js +11 -5
- package/dist/core/updater.js.map +1 -1
- package/dist/core/work-topology.d.ts.map +1 -1
- package/dist/core/work-topology.js +1 -0
- package/dist/core/work-topology.js.map +1 -1
- package/dist/host-adapters/acp-client.d.ts +147 -0
- package/dist/host-adapters/acp-client.d.ts.map +1 -0
- package/dist/host-adapters/acp-client.js +793 -0
- package/dist/host-adapters/acp-client.js.map +1 -0
- package/dist/host-adapters/claude-channel.d.ts +75 -0
- package/dist/host-adapters/claude-channel.d.ts.map +1 -0
- package/dist/host-adapters/claude-channel.js +255 -0
- package/dist/host-adapters/claude-channel.js.map +1 -0
- package/dist/host-adapters/codex-app-server.d.ts +83 -0
- package/dist/host-adapters/codex-app-server.d.ts.map +1 -0
- package/dist/host-adapters/codex-app-server.js +360 -0
- package/dist/host-adapters/codex-app-server.js.map +1 -0
- package/dist/host-adapters/codex-cli-queue.d.ts +17 -0
- package/dist/host-adapters/codex-cli-queue.d.ts.map +1 -0
- package/dist/host-adapters/codex-cli-queue.js +85 -0
- package/dist/host-adapters/codex-cli-queue.js.map +1 -0
- package/dist/host-runtime/acp.d.ts +57 -0
- package/dist/host-runtime/acp.d.ts.map +1 -0
- package/dist/host-runtime/acp.js +338 -0
- package/dist/host-runtime/acp.js.map +1 -0
- package/dist/host-runtime/claude.d.ts +41 -0
- package/dist/host-runtime/claude.d.ts.map +1 -0
- package/dist/host-runtime/claude.js +213 -0
- package/dist/host-runtime/claude.js.map +1 -0
- package/dist/host-runtime/codex-session.d.ts +24 -0
- package/dist/host-runtime/codex-session.d.ts.map +1 -0
- package/dist/host-runtime/codex-session.js +98 -0
- package/dist/host-runtime/codex-session.js.map +1 -0
- package/dist/host-runtime/codex.d.ts +33 -0
- package/dist/host-runtime/codex.d.ts.map +1 -0
- package/dist/host-runtime/codex.js +215 -0
- package/dist/host-runtime/codex.js.map +1 -0
- package/dist/host-runtime/config.d.ts +9 -0
- package/dist/host-runtime/config.d.ts.map +1 -0
- package/dist/host-runtime/config.js +91 -0
- package/dist/host-runtime/config.js.map +1 -0
- package/dist/host-runtime/router-client.d.ts +38 -0
- package/dist/host-runtime/router-client.d.ts.map +1 -0
- package/dist/host-runtime/router-client.js +463 -0
- package/dist/host-runtime/router-client.js.map +1 -0
- package/dist/host-runtime/router.d.ts +3 -0
- package/dist/host-runtime/router.d.ts.map +1 -0
- package/dist/host-runtime/router.js +43 -0
- package/dist/host-runtime/router.js.map +1 -0
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/skills-manifest.json +22 -17
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +8 -1
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/schema.d.ts +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +255 -0
- package/dist/storage/schema.js.map +1 -1
- package/dist/transports/agent-messaging.d.ts +11 -0
- package/dist/transports/agent-messaging.d.ts.map +1 -0
- package/dist/transports/agent-messaging.js +273 -0
- package/dist/transports/agent-messaging.js.map +1 -0
- package/dist/transports/cli/cli.d.ts +6 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +392 -17
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +31 -2
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +152 -1
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +108 -3
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +106 -1
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +86 -0
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +167 -0
- package/docs/platforms/agent-messaging.md +290 -0
- package/hooks/hooks.json +11 -0
- package/llms-install.md +101 -25
- package/package.json +14 -5
- package/scripts/hooks/_generated/repo-state.js +77 -0
- package/scripts/hooks/_generated/schema.js +255 -0
- package/scripts/hooks/_generated/task-state.js +3 -3
- package/scripts/hooks/_generated/work-topology.js +1 -0
- package/scripts/hooks/_shared.js +77 -97
- package/scripts/hooks/auto-update-runner.mjs +243 -0
- package/scripts/hooks/session-start.js +12 -2
- package/scripts/hooks/session-summary.js +7 -6
- package/skills/memesh/SKILL.md +40 -1
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { agentMessagePayloadStorageBytes, enforceAgentMessageStorageQuota, } from './agent-message-storage.js';
|
|
3
|
+
const MAX_SCOPE_FIELD = 200;
|
|
4
|
+
const MAX_IDEMPOTENCY_KEY = 200;
|
|
5
|
+
const MAX_CURSOR_TOKEN = 160;
|
|
6
|
+
const MAX_JSON_BYTES = 64 * 1024;
|
|
7
|
+
const DEFAULT_POLL_LIMIT = 50;
|
|
8
|
+
const MAX_POLL_LIMIT = 200;
|
|
9
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
10
|
+
const DEFAULT_WAIT_INTERVAL_MS = 100;
|
|
11
|
+
const MIN_WAIT_INTERVAL_MS = 10;
|
|
12
|
+
export class AgentMessagingError extends Error {
|
|
13
|
+
}
|
|
14
|
+
export class AgentIdempotencyConflictError extends AgentMessagingError {
|
|
15
|
+
}
|
|
16
|
+
export class AgentMessageAccessError extends AgentMessagingError {
|
|
17
|
+
}
|
|
18
|
+
export class AgentWaitAbortedError extends AgentMessagingError {
|
|
19
|
+
}
|
|
20
|
+
export function sendAgentMessage(db, input, options = {}) {
|
|
21
|
+
const normalized = normalizeSendInput(input);
|
|
22
|
+
const requestHash = hashCanonical({
|
|
23
|
+
project: normalized.project,
|
|
24
|
+
sender: normalized.sender,
|
|
25
|
+
recipient: normalized.recipient,
|
|
26
|
+
target_kind: normalized.target_kind,
|
|
27
|
+
idempotency_key: normalized.idempotency_key,
|
|
28
|
+
content_type: normalized.content_type,
|
|
29
|
+
sender_host: normalized.sender_host,
|
|
30
|
+
correlation_id: normalized.correlation_id,
|
|
31
|
+
reply_to: normalized.reply_to,
|
|
32
|
+
privacy: normalized.privacy,
|
|
33
|
+
payload: normalized.payload,
|
|
34
|
+
provenance: normalized.provenance,
|
|
35
|
+
});
|
|
36
|
+
const existing = lookupExistingMessage(db, normalized.project, normalized.sender, normalized.idempotency_key);
|
|
37
|
+
if (existing) {
|
|
38
|
+
if (existing.request_hash !== requestHash) {
|
|
39
|
+
throw new AgentIdempotencyConflictError(`Agent message idempotency conflict for sender ${normalized.sender} in project ${normalized.project}.`);
|
|
40
|
+
}
|
|
41
|
+
return finishSentMessage(existing, options);
|
|
42
|
+
}
|
|
43
|
+
const messageId = randomUUID();
|
|
44
|
+
const deliveryId = randomUUID();
|
|
45
|
+
const eventId = randomUUID();
|
|
46
|
+
const payloadJson = stableStringify(normalized.payload);
|
|
47
|
+
const tx = db.transaction(() => {
|
|
48
|
+
if (options.storage_quota_bytes !== undefined) {
|
|
49
|
+
enforceAgentMessageStorageQuota(db, {
|
|
50
|
+
quotaBytes: options.storage_quota_bytes,
|
|
51
|
+
additionalPayloadBytes: agentMessagePayloadStorageBytes(payloadJson),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
db.prepare(`
|
|
55
|
+
INSERT INTO agent_messages (
|
|
56
|
+
message_id, project, sender, sender_host, recipient, content_type,
|
|
57
|
+
correlation_id, reply_to_message_id, privacy, payload_json, provenance_json
|
|
58
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
|
+
`).run(messageId, normalized.project, normalized.sender, normalized.sender_host, normalized.recipient, normalized.content_type, normalized.correlation_id, normalized.reply_to, normalized.privacy, payloadJson, stableStringify(normalized.provenance));
|
|
60
|
+
db.prepare(`
|
|
61
|
+
INSERT INTO agent_message_deliveries (delivery_id, message_id, project, recipient, target_kind)
|
|
62
|
+
VALUES (?, ?, ?, ?, ?)
|
|
63
|
+
`).run(deliveryId, messageId, normalized.project, normalized.recipient, normalized.target_kind);
|
|
64
|
+
db.prepare(`
|
|
65
|
+
INSERT INTO agent_message_events (event_id, message_id, delivery_id, project, recipient, event_kind)
|
|
66
|
+
VALUES (?, ?, ?, ?, ?, 'message_available')
|
|
67
|
+
`).run(eventId, messageId, deliveryId, normalized.project, normalized.recipient);
|
|
68
|
+
db.prepare(`
|
|
69
|
+
INSERT INTO agent_message_idempotency (project, sender, idempotency_key, request_hash, message_id)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?)
|
|
71
|
+
`).run(normalized.project, normalized.sender, normalized.idempotency_key, requestHash, messageId);
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
tx.immediate();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!isUniqueConstraint(error))
|
|
78
|
+
throw error;
|
|
79
|
+
const raced = lookupExistingMessage(db, normalized.project, normalized.sender, normalized.idempotency_key);
|
|
80
|
+
if (raced && raced.request_hash === requestHash)
|
|
81
|
+
return finishSentMessage(raced, options);
|
|
82
|
+
if (raced) {
|
|
83
|
+
throw new AgentIdempotencyConflictError(`Agent message idempotency conflict for sender ${normalized.sender} in project ${normalized.project}.`);
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const stored = loadSentMessage(db, normalized.project, normalized.recipient, messageId);
|
|
88
|
+
if (!stored)
|
|
89
|
+
throw new AgentMessagingError(`Sent agent message ${messageId} could not be read back.`);
|
|
90
|
+
return finishSentMessage(stored, options);
|
|
91
|
+
}
|
|
92
|
+
export function pollAgentEvents(db, input) {
|
|
93
|
+
const project = requireText('project', input.project, MAX_SCOPE_FIELD);
|
|
94
|
+
const recipient = requireText('recipient', input.recipient, MAX_SCOPE_FIELD);
|
|
95
|
+
const limit = normalizeLimit(input.limit);
|
|
96
|
+
const cursor = resolveCursor(db, project, recipient, input.cursor ?? null);
|
|
97
|
+
const rows = db.prepare(`
|
|
98
|
+
SELECT
|
|
99
|
+
e.event_sequence,
|
|
100
|
+
e.event_id,
|
|
101
|
+
e.message_id,
|
|
102
|
+
m.sender,
|
|
103
|
+
m.sender_host,
|
|
104
|
+
e.recipient,
|
|
105
|
+
d.target_kind,
|
|
106
|
+
m.content_type,
|
|
107
|
+
m.correlation_id,
|
|
108
|
+
m.reply_to_message_id,
|
|
109
|
+
m.privacy,
|
|
110
|
+
e.created_at
|
|
111
|
+
FROM agent_message_events e
|
|
112
|
+
JOIN agent_messages m ON m.message_id = e.message_id
|
|
113
|
+
JOIN agent_message_deliveries d ON d.delivery_id = e.delivery_id
|
|
114
|
+
WHERE e.project = ? AND e.recipient = ? AND e.event_sequence > ?
|
|
115
|
+
ORDER BY e.event_sequence ASC
|
|
116
|
+
LIMIT ?
|
|
117
|
+
`).all(project, recipient, cursor.event_sequence, limit);
|
|
118
|
+
if (rows.length === 0) {
|
|
119
|
+
if (cursor.cursor_token)
|
|
120
|
+
return { events: [], next_cursor: cursor.cursor_token };
|
|
121
|
+
return { events: [], next_cursor: createCursor(db, project, recipient, cursor.event_sequence) };
|
|
122
|
+
}
|
|
123
|
+
const nextCursor = createCursor(db, project, recipient, rows[rows.length - 1].event_sequence);
|
|
124
|
+
return {
|
|
125
|
+
events: rows.map(rowToEventHeader),
|
|
126
|
+
next_cursor: nextCursor,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function waitForAgentEvents(db, input, signal) {
|
|
130
|
+
const timeoutMs = normalizeTimeout(input.wait_ms);
|
|
131
|
+
const pollIntervalMs = normalizePollInterval(input.poll_interval_ms);
|
|
132
|
+
const deadline = Date.now() + timeoutMs;
|
|
133
|
+
let currentCursor = input.cursor ?? null;
|
|
134
|
+
for (;;) {
|
|
135
|
+
throwIfAborted(signal);
|
|
136
|
+
const result = pollAgentEvents(db, {
|
|
137
|
+
project: input.project,
|
|
138
|
+
recipient: input.recipient,
|
|
139
|
+
cursor: currentCursor,
|
|
140
|
+
limit: input.limit,
|
|
141
|
+
});
|
|
142
|
+
if (result.events.length > 0)
|
|
143
|
+
return result;
|
|
144
|
+
currentCursor = result.next_cursor;
|
|
145
|
+
const remaining = deadline - Date.now();
|
|
146
|
+
if (remaining <= 0)
|
|
147
|
+
return result;
|
|
148
|
+
await waitForDelay(Math.min(pollIntervalMs, remaining), signal);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export function fetchAgentMessage(db, input) {
|
|
152
|
+
const project = requireText('project', input.project, MAX_SCOPE_FIELD);
|
|
153
|
+
const recipient = requireText('recipient', input.recipient, MAX_SCOPE_FIELD);
|
|
154
|
+
const messageId = requireText('message_id', input.message_id, MAX_SCOPE_FIELD);
|
|
155
|
+
const targetKind = parseTargetKind(input.target_kind ?? 'principal');
|
|
156
|
+
const row = db.prepare(`
|
|
157
|
+
SELECT
|
|
158
|
+
m.message_id,
|
|
159
|
+
m.project,
|
|
160
|
+
m.sender,
|
|
161
|
+
m.sender_host,
|
|
162
|
+
d.recipient,
|
|
163
|
+
d.target_kind,
|
|
164
|
+
m.content_type,
|
|
165
|
+
m.correlation_id,
|
|
166
|
+
m.reply_to_message_id,
|
|
167
|
+
m.privacy,
|
|
168
|
+
m.payload_json,
|
|
169
|
+
m.provenance_json,
|
|
170
|
+
m.created_at
|
|
171
|
+
FROM agent_messages m
|
|
172
|
+
JOIN agent_message_deliveries d ON d.message_id = m.message_id
|
|
173
|
+
WHERE m.project = ? AND d.project = ? AND d.recipient = ? AND d.target_kind = ? AND m.message_id = ?
|
|
174
|
+
`).get(project, project, recipient, targetKind, messageId);
|
|
175
|
+
if (!row) {
|
|
176
|
+
throw new AgentMessageAccessError(`Agent message ${messageId} is not available to recipient ${recipient} in project ${project}.`);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
message_id: row.message_id,
|
|
180
|
+
project: row.project,
|
|
181
|
+
sender: row.sender,
|
|
182
|
+
sender_host: row.sender_host,
|
|
183
|
+
recipient: row.recipient,
|
|
184
|
+
target_kind: parseTargetKind(row.target_kind),
|
|
185
|
+
content_type: parseContentType(row.content_type),
|
|
186
|
+
correlation_id: row.correlation_id,
|
|
187
|
+
reply_to: row.reply_to_message_id,
|
|
188
|
+
privacy: parsePrivacy(row.privacy),
|
|
189
|
+
created_at: row.created_at,
|
|
190
|
+
payload: parseJsonObjectOrValue(row.payload_json),
|
|
191
|
+
provenance: parseJsonObject(row.provenance_json, 'provenance_json'),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function recordAgentReceipt(db, input) {
|
|
195
|
+
const normalized = normalizeReceiptInput(input);
|
|
196
|
+
assertMessageAccess(db, normalized.project, normalized.recipient, normalized.message_id);
|
|
197
|
+
const detail = buildReceiptDetail(normalized);
|
|
198
|
+
const requestHash = hashCanonical({
|
|
199
|
+
message_id: normalized.message_id,
|
|
200
|
+
receipt_kind: normalized.receipt_kind,
|
|
201
|
+
actor: normalized.actor,
|
|
202
|
+
detail,
|
|
203
|
+
});
|
|
204
|
+
const existing = lookupExistingReceipt(db, normalized.project, normalized.recipient, normalized.message_id, normalized.receipt_kind, normalized.idempotency_key);
|
|
205
|
+
if (existing) {
|
|
206
|
+
if (existing.request_hash !== requestHash) {
|
|
207
|
+
throw new AgentIdempotencyConflictError(`Agent receipt idempotency conflict for message ${normalized.message_id} (${normalized.receipt_kind}).`);
|
|
208
|
+
}
|
|
209
|
+
return rowToReceipt(existing);
|
|
210
|
+
}
|
|
211
|
+
const receiptId = randomUUID();
|
|
212
|
+
const tx = db.transaction(() => {
|
|
213
|
+
db.prepare(`
|
|
214
|
+
INSERT INTO agent_message_receipts (
|
|
215
|
+
receipt_id, message_id, project, recipient, receipt_kind, actor, idempotency_key, request_hash, detail_json
|
|
216
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
217
|
+
`).run(receiptId, normalized.message_id, normalized.project, normalized.recipient, normalized.receipt_kind, normalized.actor, normalized.idempotency_key, requestHash, stableStringify(detail));
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
tx.immediate();
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (!isUniqueConstraint(error))
|
|
224
|
+
throw error;
|
|
225
|
+
const raced = lookupExistingReceipt(db, normalized.project, normalized.recipient, normalized.message_id, normalized.receipt_kind, normalized.idempotency_key);
|
|
226
|
+
if (raced && raced.request_hash === requestHash)
|
|
227
|
+
return rowToReceipt(raced);
|
|
228
|
+
if (raced) {
|
|
229
|
+
throw new AgentIdempotencyConflictError(`Agent receipt idempotency conflict for message ${normalized.message_id} (${normalized.receipt_kind}).`);
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
const stored = lookupExistingReceipt(db, normalized.project, normalized.recipient, normalized.message_id, normalized.receipt_kind, normalized.idempotency_key);
|
|
234
|
+
if (!stored)
|
|
235
|
+
throw new AgentMessagingError(`Agent receipt ${receiptId} could not be read back.`);
|
|
236
|
+
return rowToReceipt(stored);
|
|
237
|
+
}
|
|
238
|
+
export function readAgentMessageReceipts(db, input) {
|
|
239
|
+
const project = requireText('project', input.project, MAX_SCOPE_FIELD);
|
|
240
|
+
const recipient = requireText('recipient', input.recipient, MAX_SCOPE_FIELD);
|
|
241
|
+
const messageId = requireText('message_id', input.message_id, MAX_SCOPE_FIELD);
|
|
242
|
+
assertMessageAccess(db, project, recipient, messageId);
|
|
243
|
+
const rows = db.prepare(`
|
|
244
|
+
SELECT receipt_id, message_id, project, recipient, receipt_kind, actor, idempotency_key, detail_json, created_at, request_hash
|
|
245
|
+
FROM agent_message_receipts
|
|
246
|
+
WHERE project = ? AND recipient = ? AND message_id = ?
|
|
247
|
+
ORDER BY rowid ASC
|
|
248
|
+
`).all(project, recipient, messageId);
|
|
249
|
+
return rows.map(rowToReceipt);
|
|
250
|
+
}
|
|
251
|
+
export function recordAgentAckFact(db, input) {
|
|
252
|
+
const deliveryId = requireText('delivery_id', input.delivery_id, MAX_SCOPE_FIELD);
|
|
253
|
+
const hostAcceptId = requireText('host_accept_id', input.host_accept_id, MAX_SCOPE_FIELD);
|
|
254
|
+
const actor = requireText('actor', input.actor, MAX_SCOPE_FIELD);
|
|
255
|
+
const idempotencyKey = requireText('idempotency_key', input.idempotency_key, MAX_IDEMPOTENCY_KEY);
|
|
256
|
+
const detail = normalizeBoundedDetail(input.detail);
|
|
257
|
+
const requestHash = hashCanonical({ delivery_id: deliveryId, host_accept_id: hostAcceptId, actor, detail });
|
|
258
|
+
const accepted = db.prepare(`
|
|
259
|
+
SELECT 1 FROM agent_host_accepts WHERE host_accept_id = ? AND delivery_id = ?
|
|
260
|
+
`).get(hostAcceptId, deliveryId);
|
|
261
|
+
if (!accepted) {
|
|
262
|
+
throw new AgentMessageAccessError(`Host acceptance ${hostAcceptId} does not belong to delivery ${deliveryId}.`);
|
|
263
|
+
}
|
|
264
|
+
const existing = lookupAckFact(db, deliveryId, actor, idempotencyKey);
|
|
265
|
+
if (existing)
|
|
266
|
+
return checkedFact(existing, requestHash, rowToAckFact, deliveryId, 'agent_ack');
|
|
267
|
+
const factId = randomUUID();
|
|
268
|
+
try {
|
|
269
|
+
db.prepare(`
|
|
270
|
+
INSERT INTO agent_ack_facts (
|
|
271
|
+
ack_fact_id, delivery_id, host_accept_id, actor, idempotency_key, request_hash, detail_json
|
|
272
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
273
|
+
`).run(factId, deliveryId, hostAcceptId, actor, idempotencyKey, requestHash, stableStringify(detail));
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (!isUniqueConstraint(error))
|
|
277
|
+
throw error;
|
|
278
|
+
const raced = lookupAckFact(db, deliveryId, actor, idempotencyKey);
|
|
279
|
+
if (raced)
|
|
280
|
+
return checkedFact(raced, requestHash, rowToAckFact, deliveryId, 'agent_ack');
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
const stored = lookupAckFact(db, deliveryId, actor, idempotencyKey);
|
|
284
|
+
if (!stored)
|
|
285
|
+
throw new AgentMessagingError(`Agent acknowledgement ${factId} could not be read back.`);
|
|
286
|
+
return rowToAckFact(stored);
|
|
287
|
+
}
|
|
288
|
+
export function recordAgentWorkflowFact(db, input) {
|
|
289
|
+
const deliveryId = requireText('delivery_id', input.delivery_id, MAX_SCOPE_FIELD);
|
|
290
|
+
const actor = requireText('actor', input.actor, MAX_SCOPE_FIELD);
|
|
291
|
+
const workflowState = requireText('workflow_state', input.workflow_state, MAX_SCOPE_FIELD);
|
|
292
|
+
const idempotencyKey = requireText('idempotency_key', input.idempotency_key, MAX_IDEMPOTENCY_KEY);
|
|
293
|
+
const detail = normalizeBoundedDetail(input.detail);
|
|
294
|
+
assertDeliveryExists(db, deliveryId);
|
|
295
|
+
const requestHash = hashCanonical({ delivery_id: deliveryId, actor, workflow_state: workflowState, detail });
|
|
296
|
+
const existing = lookupWorkflowFact(db, deliveryId, actor, idempotencyKey);
|
|
297
|
+
if (existing)
|
|
298
|
+
return checkedFact(existing, requestHash, rowToWorkflowFact, deliveryId, 'workflow');
|
|
299
|
+
const factId = randomUUID();
|
|
300
|
+
try {
|
|
301
|
+
db.prepare(`
|
|
302
|
+
INSERT INTO agent_workflow_facts (
|
|
303
|
+
workflow_fact_id, delivery_id, actor, workflow_state, idempotency_key, request_hash, detail_json
|
|
304
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
305
|
+
`).run(factId, deliveryId, actor, workflowState, idempotencyKey, requestHash, stableStringify(detail));
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (!isUniqueConstraint(error))
|
|
309
|
+
throw error;
|
|
310
|
+
const raced = lookupWorkflowFact(db, deliveryId, actor, idempotencyKey);
|
|
311
|
+
if (raced)
|
|
312
|
+
return checkedFact(raced, requestHash, rowToWorkflowFact, deliveryId, 'workflow');
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
const stored = lookupWorkflowFact(db, deliveryId, actor, idempotencyKey);
|
|
316
|
+
if (!stored)
|
|
317
|
+
throw new AgentMessagingError(`Agent workflow fact ${factId} could not be read back.`);
|
|
318
|
+
return rowToWorkflowFact(stored);
|
|
319
|
+
}
|
|
320
|
+
export function recordAgentRetentionFact(db, input) {
|
|
321
|
+
const messageId = requireText('message_id', input.message_id, MAX_SCOPE_FIELD);
|
|
322
|
+
const actor = requireText('actor', input.actor, MAX_SCOPE_FIELD);
|
|
323
|
+
const retentionState = requireText('retention_state', input.retention_state, MAX_SCOPE_FIELD);
|
|
324
|
+
const idempotencyKey = requireText('idempotency_key', input.idempotency_key, MAX_IDEMPOTENCY_KEY);
|
|
325
|
+
const detail = normalizeBoundedDetail(input.detail);
|
|
326
|
+
if (!db.prepare('SELECT 1 FROM agent_messages WHERE message_id = ?').get(messageId)) {
|
|
327
|
+
throw new AgentMessageAccessError(`Agent message ${messageId} does not exist.`);
|
|
328
|
+
}
|
|
329
|
+
const requestHash = hashCanonical({ message_id: messageId, actor, retention_state: retentionState, detail });
|
|
330
|
+
const existing = lookupRetentionFact(db, messageId, actor, idempotencyKey);
|
|
331
|
+
if (existing)
|
|
332
|
+
return checkedFact(existing, requestHash, rowToRetentionFact, messageId, 'retention');
|
|
333
|
+
const factId = randomUUID();
|
|
334
|
+
try {
|
|
335
|
+
db.prepare(`
|
|
336
|
+
INSERT INTO agent_retention_facts (
|
|
337
|
+
retention_fact_id, message_id, actor, retention_state, idempotency_key, request_hash, detail_json
|
|
338
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
339
|
+
`).run(factId, messageId, actor, retentionState, idempotencyKey, requestHash, stableStringify(detail));
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (!isUniqueConstraint(error))
|
|
343
|
+
throw error;
|
|
344
|
+
const raced = lookupRetentionFact(db, messageId, actor, idempotencyKey);
|
|
345
|
+
if (raced)
|
|
346
|
+
return checkedFact(raced, requestHash, rowToRetentionFact, messageId, 'retention');
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
const stored = lookupRetentionFact(db, messageId, actor, idempotencyKey);
|
|
350
|
+
if (!stored)
|
|
351
|
+
throw new AgentMessagingError(`Agent retention fact ${factId} could not be read back.`);
|
|
352
|
+
return rowToRetentionFact(stored);
|
|
353
|
+
}
|
|
354
|
+
function normalizeSendInput(input) {
|
|
355
|
+
const project = requireText('project', input.project, MAX_SCOPE_FIELD);
|
|
356
|
+
const sender = requireText('sender', input.sender, MAX_SCOPE_FIELD);
|
|
357
|
+
const recipient = requireText('recipient', input.recipient, MAX_SCOPE_FIELD);
|
|
358
|
+
const target_kind = parseTargetKind(input.target_kind ?? 'principal');
|
|
359
|
+
const idempotency_key = requireText('idempotency_key', input.idempotency_key, MAX_IDEMPOTENCY_KEY);
|
|
360
|
+
const content_type = parseContentType(input.content_type);
|
|
361
|
+
const sender_host = optionalText('sender_host', input.sender_host ?? null, MAX_SCOPE_FIELD);
|
|
362
|
+
const correlation_id = optionalText('correlation_id', input.correlation_id ?? null, MAX_SCOPE_FIELD);
|
|
363
|
+
const reply_to = optionalText('reply_to', input.reply_to ?? null, MAX_SCOPE_FIELD);
|
|
364
|
+
const privacy = parsePrivacy(input.privacy ?? 'private');
|
|
365
|
+
if (content_type === 'text/plain' && typeof input.payload !== 'string') {
|
|
366
|
+
throw new AgentMessagingError('text/plain agent messages require a string payload.');
|
|
367
|
+
}
|
|
368
|
+
assertJsonValue(input.payload, 'payload');
|
|
369
|
+
const payloadBytes = Buffer.byteLength(stableStringify(input.payload), 'utf8');
|
|
370
|
+
if (payloadBytes > MAX_JSON_BYTES) {
|
|
371
|
+
throw new AgentMessagingError(`Agent message payload exceeds ${MAX_JSON_BYTES} bytes.`);
|
|
372
|
+
}
|
|
373
|
+
const provenance = input.provenance === undefined
|
|
374
|
+
? {}
|
|
375
|
+
: normalizeObject(input.provenance);
|
|
376
|
+
const provenanceBytes = Buffer.byteLength(stableStringify(provenance), 'utf8');
|
|
377
|
+
if (provenanceBytes > MAX_JSON_BYTES) {
|
|
378
|
+
throw new AgentMessagingError(`Agent message provenance exceeds ${MAX_JSON_BYTES} bytes.`);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
project,
|
|
382
|
+
sender,
|
|
383
|
+
recipient,
|
|
384
|
+
target_kind,
|
|
385
|
+
idempotency_key,
|
|
386
|
+
content_type,
|
|
387
|
+
payload: input.payload,
|
|
388
|
+
sender_host,
|
|
389
|
+
correlation_id,
|
|
390
|
+
reply_to,
|
|
391
|
+
privacy,
|
|
392
|
+
provenance,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function normalizeReceiptInput(input) {
|
|
396
|
+
const project = requireText('project', input.project, MAX_SCOPE_FIELD);
|
|
397
|
+
const recipient = requireText('recipient', input.recipient, MAX_SCOPE_FIELD);
|
|
398
|
+
const message_id = requireText('message_id', input.message_id, MAX_SCOPE_FIELD);
|
|
399
|
+
const actor = requireText('actor', input.actor, MAX_SCOPE_FIELD);
|
|
400
|
+
const idempotency_key = requireText('idempotency_key', input.idempotency_key, MAX_IDEMPOTENCY_KEY);
|
|
401
|
+
const detail = input.detail === undefined
|
|
402
|
+
? {}
|
|
403
|
+
: normalizeObject(input.detail);
|
|
404
|
+
const detailBytes = Buffer.byteLength(stableStringify(detail), 'utf8');
|
|
405
|
+
if (detailBytes > MAX_JSON_BYTES) {
|
|
406
|
+
throw new AgentMessagingError(`Agent receipt detail exceeds ${MAX_JSON_BYTES} bytes.`);
|
|
407
|
+
}
|
|
408
|
+
switch (input.receipt_kind) {
|
|
409
|
+
case 'intake':
|
|
410
|
+
if (input.intake_state !== 'fetched' && input.intake_state !== 'ingested') {
|
|
411
|
+
throw new AgentMessagingError(`Unsupported intake_state ${String(input.intake_state)}.`);
|
|
412
|
+
}
|
|
413
|
+
return { ...input, project, recipient, message_id, actor, idempotency_key, detail };
|
|
414
|
+
case 'ack':
|
|
415
|
+
return { ...input, project, recipient, message_id, actor, idempotency_key, detail };
|
|
416
|
+
case 'disposition':
|
|
417
|
+
if (!['accepted', 'rejected', 'cancelled', 'completed', 'deferred'].includes(input.disposition)) {
|
|
418
|
+
throw new AgentMessagingError(`Unsupported disposition ${String(input.disposition)}.`);
|
|
419
|
+
}
|
|
420
|
+
return { ...input, project, recipient, message_id, actor, idempotency_key, detail };
|
|
421
|
+
case 'host_activation':
|
|
422
|
+
if (!['woken', 'manual_resume_required', 'unsupported', 'failed'].includes(input.host_activation)) {
|
|
423
|
+
throw new AgentMessagingError(`Unsupported host_activation ${String(input.host_activation)}.`);
|
|
424
|
+
}
|
|
425
|
+
return { ...input, project, recipient, message_id, actor, idempotency_key, detail };
|
|
426
|
+
default:
|
|
427
|
+
throw new AgentMessagingError(`Unsupported receipt_kind ${input.receipt_kind ?? '<missing>'}.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function buildReceiptDetail(input) {
|
|
431
|
+
const detail = input.detail === undefined
|
|
432
|
+
? {}
|
|
433
|
+
: normalizeObject(input.detail);
|
|
434
|
+
switch (input.receipt_kind) {
|
|
435
|
+
case 'intake':
|
|
436
|
+
return { intake_state: input.intake_state, detail };
|
|
437
|
+
case 'ack':
|
|
438
|
+
return { acknowledged: true, detail };
|
|
439
|
+
case 'disposition':
|
|
440
|
+
return { disposition: input.disposition, detail };
|
|
441
|
+
case 'host_activation':
|
|
442
|
+
return { host_activation: input.host_activation, detail };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function normalizeLimit(limit) {
|
|
446
|
+
if (limit === undefined)
|
|
447
|
+
return DEFAULT_POLL_LIMIT;
|
|
448
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_POLL_LIMIT) {
|
|
449
|
+
throw new AgentMessagingError(`Poll limit must be an integer between 1 and ${MAX_POLL_LIMIT}.`);
|
|
450
|
+
}
|
|
451
|
+
return limit;
|
|
452
|
+
}
|
|
453
|
+
function normalizeTimeout(timeoutMs) {
|
|
454
|
+
if (timeoutMs === undefined)
|
|
455
|
+
return DEFAULT_WAIT_TIMEOUT_MS;
|
|
456
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 5 * 60 * 1000) {
|
|
457
|
+
throw new AgentMessagingError('wait_ms must be an integer between 0 and 300000.');
|
|
458
|
+
}
|
|
459
|
+
return timeoutMs;
|
|
460
|
+
}
|
|
461
|
+
function normalizePollInterval(intervalMs) {
|
|
462
|
+
if (intervalMs === undefined)
|
|
463
|
+
return DEFAULT_WAIT_INTERVAL_MS;
|
|
464
|
+
if (!Number.isInteger(intervalMs) || intervalMs < MIN_WAIT_INTERVAL_MS || intervalMs > 60_000) {
|
|
465
|
+
throw new AgentMessagingError(`poll_interval_ms must be an integer between ${MIN_WAIT_INTERVAL_MS} and 60000.`);
|
|
466
|
+
}
|
|
467
|
+
return intervalMs;
|
|
468
|
+
}
|
|
469
|
+
function resolveCursor(db, project, recipient, cursorToken) {
|
|
470
|
+
if (!cursorToken) {
|
|
471
|
+
return { cursor_token: '', project, recipient, event_sequence: 0 };
|
|
472
|
+
}
|
|
473
|
+
const token = requireText('cursor', cursorToken, MAX_CURSOR_TOKEN);
|
|
474
|
+
const row = db.prepare(`
|
|
475
|
+
SELECT cursor_token, project, recipient, event_sequence
|
|
476
|
+
FROM agent_message_cursors
|
|
477
|
+
WHERE cursor_token = ?
|
|
478
|
+
`).get(token);
|
|
479
|
+
if (!row || row.project !== project || row.recipient !== recipient) {
|
|
480
|
+
throw new AgentMessageAccessError(`Agent message cursor is not available to recipient ${recipient} in project ${project}.`);
|
|
481
|
+
}
|
|
482
|
+
return row;
|
|
483
|
+
}
|
|
484
|
+
function createCursor(db, project, recipient, eventSequence) {
|
|
485
|
+
const existing = db.prepare(`
|
|
486
|
+
SELECT cursor_token
|
|
487
|
+
FROM agent_message_cursors
|
|
488
|
+
WHERE project = ? AND recipient = ? AND event_sequence = ?
|
|
489
|
+
LIMIT 1
|
|
490
|
+
`).get(project, recipient, eventSequence);
|
|
491
|
+
if (existing)
|
|
492
|
+
return existing.cursor_token;
|
|
493
|
+
const cursorToken = randomBytes(18).toString('base64url');
|
|
494
|
+
try {
|
|
495
|
+
db.prepare(`
|
|
496
|
+
INSERT INTO agent_message_cursors (cursor_token, project, recipient, event_sequence)
|
|
497
|
+
VALUES (?, ?, ?, ?)
|
|
498
|
+
`).run(cursorToken, project, recipient, eventSequence);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (!isUniqueConstraint(error))
|
|
502
|
+
throw error;
|
|
503
|
+
const raced = db.prepare(`
|
|
504
|
+
SELECT cursor_token
|
|
505
|
+
FROM agent_message_cursors
|
|
506
|
+
WHERE project = ? AND recipient = ? AND event_sequence = ?
|
|
507
|
+
LIMIT 1
|
|
508
|
+
`).get(project, recipient, eventSequence);
|
|
509
|
+
if (raced)
|
|
510
|
+
return raced.cursor_token;
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
return cursorToken;
|
|
514
|
+
}
|
|
515
|
+
function lookupExistingMessage(db, project, sender, idempotencyKey) {
|
|
516
|
+
return db.prepare(`
|
|
517
|
+
SELECT
|
|
518
|
+
i.request_hash,
|
|
519
|
+
m.message_id,
|
|
520
|
+
d.delivery_id,
|
|
521
|
+
e.event_id,
|
|
522
|
+
m.project,
|
|
523
|
+
m.sender,
|
|
524
|
+
m.sender_host,
|
|
525
|
+
d.recipient,
|
|
526
|
+
d.target_kind,
|
|
527
|
+
m.content_type,
|
|
528
|
+
m.correlation_id,
|
|
529
|
+
m.reply_to_message_id,
|
|
530
|
+
m.privacy,
|
|
531
|
+
m.provenance_json,
|
|
532
|
+
m.created_at
|
|
533
|
+
FROM agent_message_idempotency i
|
|
534
|
+
JOIN agent_messages m ON m.message_id = i.message_id
|
|
535
|
+
JOIN agent_message_deliveries d ON d.message_id = m.message_id
|
|
536
|
+
JOIN agent_message_events e ON e.message_id = m.message_id
|
|
537
|
+
WHERE i.project = ? AND i.sender = ? AND i.idempotency_key = ?
|
|
538
|
+
ORDER BY e.event_sequence ASC
|
|
539
|
+
LIMIT 1
|
|
540
|
+
`).get(project, sender, idempotencyKey);
|
|
541
|
+
}
|
|
542
|
+
function loadSentMessage(db, project, recipient, messageId) {
|
|
543
|
+
return db.prepare(`
|
|
544
|
+
SELECT
|
|
545
|
+
m.message_id,
|
|
546
|
+
d.delivery_id,
|
|
547
|
+
e.event_id,
|
|
548
|
+
m.project,
|
|
549
|
+
m.sender,
|
|
550
|
+
m.sender_host,
|
|
551
|
+
d.recipient,
|
|
552
|
+
d.target_kind,
|
|
553
|
+
m.content_type,
|
|
554
|
+
m.correlation_id,
|
|
555
|
+
m.reply_to_message_id,
|
|
556
|
+
m.privacy,
|
|
557
|
+
m.provenance_json,
|
|
558
|
+
m.created_at
|
|
559
|
+
FROM agent_messages m
|
|
560
|
+
JOIN agent_message_deliveries d ON d.message_id = m.message_id
|
|
561
|
+
JOIN agent_message_events e ON e.message_id = m.message_id
|
|
562
|
+
WHERE m.project = ? AND d.project = ? AND d.recipient = ? AND m.message_id = ?
|
|
563
|
+
ORDER BY e.event_sequence ASC
|
|
564
|
+
LIMIT 1
|
|
565
|
+
`).get(project, project, recipient, messageId);
|
|
566
|
+
}
|
|
567
|
+
function assertMessageAccess(db, project, recipient, messageId) {
|
|
568
|
+
const row = db.prepare(`
|
|
569
|
+
SELECT 1
|
|
570
|
+
FROM agent_message_deliveries
|
|
571
|
+
WHERE project = ? AND recipient = ? AND message_id = ?
|
|
572
|
+
`).get(project, recipient, messageId);
|
|
573
|
+
if (!row) {
|
|
574
|
+
throw new AgentMessageAccessError(`Agent message ${messageId} is not available to recipient ${recipient} in project ${project}.`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function lookupExistingReceipt(db, project, recipient, messageId, receiptKind, idempotencyKey) {
|
|
578
|
+
return db.prepare(`
|
|
579
|
+
SELECT receipt_id, message_id, project, recipient, receipt_kind, actor, idempotency_key, detail_json, created_at, request_hash
|
|
580
|
+
FROM agent_message_receipts
|
|
581
|
+
WHERE project = ? AND recipient = ? AND message_id = ? AND receipt_kind = ? AND idempotency_key = ?
|
|
582
|
+
`).get(project, recipient, messageId, receiptKind, idempotencyKey);
|
|
583
|
+
}
|
|
584
|
+
function lookupAckFact(db, deliveryId, actor, idempotencyKey) {
|
|
585
|
+
return db.prepare(`
|
|
586
|
+
SELECT ack_fact_id, delivery_id, host_accept_id, actor, idempotency_key,
|
|
587
|
+
detail_json, created_at, request_hash
|
|
588
|
+
FROM agent_ack_facts
|
|
589
|
+
WHERE delivery_id = ? AND actor = ? AND idempotency_key = ?
|
|
590
|
+
`).get(deliveryId, actor, idempotencyKey);
|
|
591
|
+
}
|
|
592
|
+
function lookupWorkflowFact(db, deliveryId, actor, idempotencyKey) {
|
|
593
|
+
return db.prepare(`
|
|
594
|
+
SELECT workflow_fact_id, delivery_id, actor, workflow_state, idempotency_key,
|
|
595
|
+
detail_json, created_at, request_hash
|
|
596
|
+
FROM agent_workflow_facts
|
|
597
|
+
WHERE delivery_id = ? AND actor = ? AND idempotency_key = ?
|
|
598
|
+
`).get(deliveryId, actor, idempotencyKey);
|
|
599
|
+
}
|
|
600
|
+
function lookupRetentionFact(db, messageId, actor, idempotencyKey) {
|
|
601
|
+
return db.prepare(`
|
|
602
|
+
SELECT retention_fact_id, message_id, actor, retention_state, idempotency_key,
|
|
603
|
+
detail_json, created_at, request_hash
|
|
604
|
+
FROM agent_retention_facts
|
|
605
|
+
WHERE message_id = ? AND actor = ? AND idempotency_key = ?
|
|
606
|
+
`).get(messageId, actor, idempotencyKey);
|
|
607
|
+
}
|
|
608
|
+
function checkedFact(row, requestHash, convert, subjectId, kind) {
|
|
609
|
+
if (row.request_hash !== requestHash) {
|
|
610
|
+
throw new AgentIdempotencyConflictError(`Agent ${kind} idempotency conflict for ${subjectId}.`);
|
|
611
|
+
}
|
|
612
|
+
return convert(row);
|
|
613
|
+
}
|
|
614
|
+
function assertDeliveryExists(db, deliveryId) {
|
|
615
|
+
if (!db.prepare('SELECT 1 FROM agent_message_deliveries WHERE delivery_id = ?').get(deliveryId)) {
|
|
616
|
+
throw new AgentMessageAccessError(`Agent message delivery ${deliveryId} does not exist.`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function normalizeBoundedDetail(detail) {
|
|
620
|
+
const normalized = detail === undefined ? {} : normalizeObject(detail);
|
|
621
|
+
if (Buffer.byteLength(stableStringify(normalized), 'utf8') > MAX_JSON_BYTES) {
|
|
622
|
+
throw new AgentMessagingError(`Agent lifecycle detail exceeds ${MAX_JSON_BYTES} bytes.`);
|
|
623
|
+
}
|
|
624
|
+
return normalized;
|
|
625
|
+
}
|
|
626
|
+
function rowToAckFact(row) {
|
|
627
|
+
return {
|
|
628
|
+
ack_fact_id: row.ack_fact_id,
|
|
629
|
+
delivery_id: row.delivery_id,
|
|
630
|
+
host_accept_id: row.host_accept_id,
|
|
631
|
+
actor: row.actor,
|
|
632
|
+
idempotency_key: row.idempotency_key,
|
|
633
|
+
detail: parseJsonObject(row.detail_json, 'detail_json'),
|
|
634
|
+
created_at: row.created_at,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function rowToWorkflowFact(row) {
|
|
638
|
+
return {
|
|
639
|
+
workflow_fact_id: row.workflow_fact_id,
|
|
640
|
+
delivery_id: row.delivery_id,
|
|
641
|
+
actor: row.actor,
|
|
642
|
+
workflow_state: row.workflow_state,
|
|
643
|
+
idempotency_key: row.idempotency_key,
|
|
644
|
+
detail: parseJsonObject(row.detail_json, 'detail_json'),
|
|
645
|
+
created_at: row.created_at,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function rowToRetentionFact(row) {
|
|
649
|
+
return {
|
|
650
|
+
retention_fact_id: row.retention_fact_id,
|
|
651
|
+
message_id: row.message_id,
|
|
652
|
+
actor: row.actor,
|
|
653
|
+
retention_state: row.retention_state,
|
|
654
|
+
idempotency_key: row.idempotency_key,
|
|
655
|
+
detail: parseJsonObject(row.detail_json, 'detail_json'),
|
|
656
|
+
created_at: row.created_at,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function finishSentMessage(row, options) {
|
|
660
|
+
const sent = rowToSentAgentMessage(row);
|
|
661
|
+
if (!options.notifier)
|
|
662
|
+
return sent;
|
|
663
|
+
try {
|
|
664
|
+
const result = options.notifier.notify({
|
|
665
|
+
delivery_id: sent.delivery_id,
|
|
666
|
+
event_id: sent.event_id,
|
|
667
|
+
project: sent.project,
|
|
668
|
+
target_kind: sent.target_kind,
|
|
669
|
+
target_id: sent.recipient,
|
|
670
|
+
});
|
|
671
|
+
if (result && typeof result.then === 'function')
|
|
672
|
+
void result.catch(() => undefined);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
}
|
|
676
|
+
return sent;
|
|
677
|
+
}
|
|
678
|
+
function rowToSentAgentMessage(row) {
|
|
679
|
+
return {
|
|
680
|
+
message_id: row.message_id,
|
|
681
|
+
delivery_id: row.delivery_id,
|
|
682
|
+
event_id: row.event_id,
|
|
683
|
+
project: row.project,
|
|
684
|
+
sender: row.sender,
|
|
685
|
+
sender_host: row.sender_host,
|
|
686
|
+
recipient: row.recipient,
|
|
687
|
+
target_kind: parseTargetKind(row.target_kind),
|
|
688
|
+
content_type: parseContentType(row.content_type),
|
|
689
|
+
correlation_id: row.correlation_id,
|
|
690
|
+
reply_to: row.reply_to_message_id,
|
|
691
|
+
privacy: parsePrivacy(row.privacy),
|
|
692
|
+
created_at: row.created_at,
|
|
693
|
+
provenance: parseJsonObject(row.provenance_json, 'provenance_json'),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function rowToEventHeader(row) {
|
|
697
|
+
return {
|
|
698
|
+
event_id: row.event_id,
|
|
699
|
+
message_id: row.message_id,
|
|
700
|
+
sender: row.sender,
|
|
701
|
+
sender_host: row.sender_host,
|
|
702
|
+
recipient: row.recipient,
|
|
703
|
+
target_kind: parseTargetKind(row.target_kind),
|
|
704
|
+
content_type: parseContentType(row.content_type),
|
|
705
|
+
correlation_id: row.correlation_id,
|
|
706
|
+
reply_to: row.reply_to_message_id,
|
|
707
|
+
privacy: parsePrivacy(row.privacy),
|
|
708
|
+
created_at: row.created_at,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function rowToReceipt(row) {
|
|
712
|
+
const detail = parseJsonObject(row.detail_json, 'detail_json');
|
|
713
|
+
const base = {
|
|
714
|
+
receipt_id: row.receipt_id,
|
|
715
|
+
message_id: row.message_id,
|
|
716
|
+
project: row.project,
|
|
717
|
+
recipient: row.recipient,
|
|
718
|
+
actor: row.actor,
|
|
719
|
+
idempotency_key: row.idempotency_key,
|
|
720
|
+
created_at: row.created_at,
|
|
721
|
+
};
|
|
722
|
+
switch (row.receipt_kind) {
|
|
723
|
+
case 'intake': {
|
|
724
|
+
const intakeState = detail.intake_state;
|
|
725
|
+
if (intakeState !== 'fetched' && intakeState !== 'ingested') {
|
|
726
|
+
throw new AgentMessagingError(`Unsupported stored intake_state ${String(intakeState)}.`);
|
|
727
|
+
}
|
|
728
|
+
return { ...base, receipt_kind: 'intake', intake_state: intakeState, detail };
|
|
729
|
+
}
|
|
730
|
+
case 'ack':
|
|
731
|
+
return { ...base, receipt_kind: 'ack', detail };
|
|
732
|
+
case 'disposition': {
|
|
733
|
+
const disposition = detail.disposition;
|
|
734
|
+
if (!['accepted', 'rejected', 'cancelled', 'completed', 'deferred'].includes(String(disposition))) {
|
|
735
|
+
throw new AgentMessagingError(`Unsupported stored disposition ${String(disposition)}.`);
|
|
736
|
+
}
|
|
737
|
+
return { ...base, receipt_kind: 'disposition', disposition: disposition, detail };
|
|
738
|
+
}
|
|
739
|
+
case 'host_activation': {
|
|
740
|
+
const activation = detail.host_activation;
|
|
741
|
+
if (!['woken', 'manual_resume_required', 'unsupported', 'failed'].includes(String(activation))) {
|
|
742
|
+
throw new AgentMessagingError(`Unsupported stored host_activation ${String(activation)}.`);
|
|
743
|
+
}
|
|
744
|
+
return { ...base, receipt_kind: 'host_activation', host_activation: activation, detail };
|
|
745
|
+
}
|
|
746
|
+
default:
|
|
747
|
+
throw new AgentMessagingError(`Unsupported stored receipt_kind ${row.receipt_kind}.`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function parsePrivacy(value) {
|
|
751
|
+
if (value === 'private' || value === 'team')
|
|
752
|
+
return value;
|
|
753
|
+
throw new AgentMessagingError(`Unsupported agent message privacy ${value}.`);
|
|
754
|
+
}
|
|
755
|
+
function parseContentType(value) {
|
|
756
|
+
if (value === 'text/plain' || value === 'application/json')
|
|
757
|
+
return value;
|
|
758
|
+
throw new AgentMessagingError(`Unsupported agent message content_type ${value}.`);
|
|
759
|
+
}
|
|
760
|
+
function parseTargetKind(value) {
|
|
761
|
+
if (value === 'principal' || value === 'session')
|
|
762
|
+
return value;
|
|
763
|
+
throw new AgentMessagingError(`Unsupported agent message target_kind ${value}.`);
|
|
764
|
+
}
|
|
765
|
+
function parseJsonObject(json, label) {
|
|
766
|
+
const parsed = parseJsonObjectOrValue(json);
|
|
767
|
+
if (!isPlainObject(parsed))
|
|
768
|
+
throw new AgentMessagingError(`${label} must contain a JSON object.`);
|
|
769
|
+
return parsed;
|
|
770
|
+
}
|
|
771
|
+
function parseJsonObjectOrValue(json) {
|
|
772
|
+
let parsed;
|
|
773
|
+
try {
|
|
774
|
+
parsed = JSON.parse(json);
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
throw new AgentMessagingError(`Stored agent message JSON is invalid: ${error instanceof Error ? error.message : String(error)}.`);
|
|
778
|
+
}
|
|
779
|
+
assertJsonValue(parsed, 'stored_json');
|
|
780
|
+
return parsed;
|
|
781
|
+
}
|
|
782
|
+
function hashCanonical(value) {
|
|
783
|
+
return createHash('sha256').update(stableStringify(value)).digest('hex');
|
|
784
|
+
}
|
|
785
|
+
function stableStringify(value) {
|
|
786
|
+
if (value === null)
|
|
787
|
+
return 'null';
|
|
788
|
+
if (typeof value === 'boolean')
|
|
789
|
+
return value ? 'true' : 'false';
|
|
790
|
+
if (typeof value === 'number') {
|
|
791
|
+
if (!Number.isFinite(value))
|
|
792
|
+
throw new AgentMessagingError('Only finite numbers are allowed in agent message JSON.');
|
|
793
|
+
return JSON.stringify(value);
|
|
794
|
+
}
|
|
795
|
+
if (typeof value === 'string')
|
|
796
|
+
return JSON.stringify(value);
|
|
797
|
+
if (Array.isArray(value))
|
|
798
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
799
|
+
const entries = Object.keys(value)
|
|
800
|
+
.sort()
|
|
801
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
802
|
+
return `{${entries.join(',')}}`;
|
|
803
|
+
}
|
|
804
|
+
function assertJsonValue(value, label) {
|
|
805
|
+
if (value === null)
|
|
806
|
+
return;
|
|
807
|
+
if (typeof value === 'boolean')
|
|
808
|
+
return;
|
|
809
|
+
if (typeof value === 'number') {
|
|
810
|
+
if (!Number.isFinite(value))
|
|
811
|
+
throw new AgentMessagingError(`${label} must contain only finite numbers.`);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (typeof value === 'string')
|
|
815
|
+
return;
|
|
816
|
+
if (Array.isArray(value)) {
|
|
817
|
+
value.forEach((entry, index) => assertJsonValue(entry, `${label}[${index}]`));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (isPlainObject(value)) {
|
|
821
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
822
|
+
assertJsonValue(entry, `${label}.${key}`);
|
|
823
|
+
}
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
throw new AgentMessagingError(`${label} must be JSON-serializable.`);
|
|
827
|
+
}
|
|
828
|
+
function normalizeObject(value) {
|
|
829
|
+
assertJsonValue(value, 'object');
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
function isPlainObject(value) {
|
|
833
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
834
|
+
}
|
|
835
|
+
function requireText(label, value, maxLength) {
|
|
836
|
+
if (typeof value !== 'string')
|
|
837
|
+
throw new AgentMessagingError(`${label} must be a string.`);
|
|
838
|
+
const trimmed = value.trim();
|
|
839
|
+
if (trimmed.length === 0)
|
|
840
|
+
throw new AgentMessagingError(`${label} must not be blank.`);
|
|
841
|
+
if (trimmed.length > maxLength) {
|
|
842
|
+
throw new AgentMessagingError(`${label} must be at most ${maxLength} characters.`);
|
|
843
|
+
}
|
|
844
|
+
return trimmed;
|
|
845
|
+
}
|
|
846
|
+
function optionalText(label, value, maxLength) {
|
|
847
|
+
if (value === null)
|
|
848
|
+
return null;
|
|
849
|
+
return requireText(label, value, maxLength);
|
|
850
|
+
}
|
|
851
|
+
function isUniqueConstraint(error) {
|
|
852
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
853
|
+
return /UNIQUE constraint failed|PRIMARY KEY/i.test(message);
|
|
854
|
+
}
|
|
855
|
+
function throwIfAborted(signal) {
|
|
856
|
+
if (signal?.aborted)
|
|
857
|
+
throw new AgentWaitAbortedError('Waiting for agent events was aborted.');
|
|
858
|
+
}
|
|
859
|
+
async function waitForDelay(ms, signal) {
|
|
860
|
+
if (ms <= 0)
|
|
861
|
+
return;
|
|
862
|
+
await new Promise((resolve, reject) => {
|
|
863
|
+
const timer = setTimeout(() => {
|
|
864
|
+
cleanup();
|
|
865
|
+
resolve();
|
|
866
|
+
}, ms);
|
|
867
|
+
const onAbort = () => {
|
|
868
|
+
cleanup();
|
|
869
|
+
reject(new AgentWaitAbortedError('Waiting for agent events was aborted.'));
|
|
870
|
+
};
|
|
871
|
+
const cleanup = () => {
|
|
872
|
+
clearTimeout(timer);
|
|
873
|
+
signal?.removeEventListener('abort', onAbort);
|
|
874
|
+
};
|
|
875
|
+
if (signal)
|
|
876
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
877
|
+
if (signal?.aborted)
|
|
878
|
+
onAbort();
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
//# sourceMappingURL=agent-messaging.js.map
|