a2a-exposed 0.8.16 → 0.8.18
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/lib/commands.mjs +4 -1
- package/lib/main.mjs +9 -6
- package/package.json +1 -1
- package/worker/migrations/0010_conversation_peer.sql +25 -0
- package/worker/migrations/0011_conversation_owner.sql +34 -0
- package/worker/migrations/0012_conversation_peer_owner.sql +13 -0
- package/worker/src/export.ts +1 -1
- package/worker/src/index.ts +272 -56
- package/worker/src/mcp.ts +2 -1
- package/worker/src/migrations.ts +6 -0
- package/worker/src/owner-rpc.ts +11 -5
- package/worker/src/version.ts +1 -1
- package/worker/src/wake.ts +4 -2
- package/worker/test/context-peer.test.ts +290 -0
- package/worker/test/facade.test.ts +6 -2
- package/worker/test/list-tasks.test.ts +120 -0
- package/worker/test/migrations.test.ts +52 -5
- package/worker/test/owner-rpc.test.ts +4 -0
- package/worker/test/push-config.test.ts +161 -0
- package/worker/test/wake.test.ts +2 -2
package/lib/commands.mjs
CHANGED
|
@@ -55,6 +55,7 @@ async function inboxRows(o) {
|
|
|
55
55
|
return followInbox(async (cursor) => {
|
|
56
56
|
const qs = [];
|
|
57
57
|
if (o.context) qs.push("context=" + q(o.context));
|
|
58
|
+
if (o.from) qs.push("from=" + q(o.from));
|
|
58
59
|
if (o.all) qs.push("all=1");
|
|
59
60
|
if (cursor) qs.push("cursor=" + q(cursor));
|
|
60
61
|
const path = "/owner/inbox" + (qs.length ? "?" + qs.join("&") : "");
|
|
@@ -125,7 +126,9 @@ export async function reply(id, o) {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
export async function history(ctx, o) {
|
|
128
|
-
const
|
|
129
|
+
const qs = new URLSearchParams({ n: String(Number(o.n || 50)) });
|
|
130
|
+
if (o.from) qs.set("from", o.from);
|
|
131
|
+
const rows = await owner("GET", `/owner/history/${q(ctx)}?${qs}`);
|
|
129
132
|
if (o.json) return out(rows);
|
|
130
133
|
for (const e of rows) {
|
|
131
134
|
const tag = [e.event, e.state || e.role].filter(Boolean).join("/");
|
package/lib/main.mjs
CHANGED
|
@@ -165,9 +165,11 @@ Status
|
|
|
165
165
|
config print config (secrets masked) and its location
|
|
166
166
|
|
|
167
167
|
Inbox (owner side)
|
|
168
|
-
inbox [--context C] [--all] [--json]
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
inbox [--context C] [--from PEER] [--all] [--json]
|
|
169
|
+
unhandled inbound tasks, and a reply to a message you sent until
|
|
170
|
+
this list is read (peer text is UNTRUSTED). --from is one peer
|
|
171
|
+
(the wake hint includes it). A reply line says to read it with
|
|
172
|
+
history and continue with send. Then pending pairing
|
|
171
173
|
requests (tell your human; the dashboard URL when MANAGE_URL is set;
|
|
172
174
|
--json: rows on stdout, requests on stderr). Follows every page; past
|
|
173
175
|
10000 pages it errors instead of a short list
|
|
@@ -175,7 +177,8 @@ Inbox (owner side)
|
|
|
175
177
|
working <taskId> mark task working
|
|
176
178
|
reply <taskId> (--text T | --stdin) [--state completed|input-required|rejected|failed|working]
|
|
177
179
|
[--artifact] [--artifact-name N] [--force]
|
|
178
|
-
history <contextId> [-n 50] [--json]
|
|
180
|
+
history <contextId> [--from PEER] [-n 50] [--json]
|
|
181
|
+
conversation log. --from is one peer (the wake hint includes it)
|
|
179
182
|
contexts recent conversations
|
|
180
183
|
export [--sql] JSON dump of this A2A endpoint on stdout (peer-token hashes and
|
|
181
184
|
encrypted peer tokens are in the dump; the owner token stays
|
|
@@ -312,9 +315,9 @@ const deployOpts = {
|
|
|
312
315
|
};
|
|
313
316
|
const SPEC = {
|
|
314
317
|
init: { ...deployOpts, ...tunnelOpts, tunnel: B }, deploy: deployOpts, wake: deployOpts, tunnel: tunnelOpts,
|
|
315
|
-
inbox: { context: S, all: B, json: B },
|
|
318
|
+
inbox: { context: S, from: S, all: B, json: B },
|
|
316
319
|
reply: { text: S, stdin: B, state: { type: "string", default: "completed" }, artifact: B, "artifact-name": { type: "string", default: "response" }, force: B },
|
|
317
|
-
history: { n: { type: "string", short: "n", default: "50" }, json: B },
|
|
320
|
+
history: { from: S, n: { type: "string", short: "n", default: "50" }, json: B },
|
|
318
321
|
token: { rotate: B },
|
|
319
322
|
peers: { "token-env": S, "token-stdin": B },
|
|
320
323
|
send: { to: S, text: S, context: S, task: S, push: B, proto: S },
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- A wake is one peer's conversation. Two peers can use the same context id without sharing a row.
|
|
2
|
+
CREATE TABLE wakes_peer (
|
|
3
|
+
context_id TEXT NOT NULL,
|
|
4
|
+
peer TEXT NOT NULL DEFAULT '',
|
|
5
|
+
last_sent_ms INTEGER NOT NULL DEFAULT 0,
|
|
6
|
+
pending_json TEXT,
|
|
7
|
+
pending_since_ms INTEGER,
|
|
8
|
+
PRIMARY KEY (context_id, peer)
|
|
9
|
+
);
|
|
10
|
+
INSERT INTO wakes_peer (context_id, peer, last_sent_ms, pending_json, pending_since_ms)
|
|
11
|
+
SELECT
|
|
12
|
+
w.context_id,
|
|
13
|
+
COALESCE(
|
|
14
|
+
NULLIF(json_extract(w.pending_json, '$.from'), ''),
|
|
15
|
+
(SELECT t.peer FROM tasks t WHERE t.context_id = w.context_id AND t.peer IS NOT NULL AND t.peer != '' ORDER BY t.created_at, t.id LIMIT 1),
|
|
16
|
+
(SELECT h.peer FROM history h WHERE h.context_id = w.context_id AND h.peer IS NOT NULL AND h.peer != '' ORDER BY h.seq LIMIT 1),
|
|
17
|
+
''
|
|
18
|
+
),
|
|
19
|
+
w.last_sent_ms,
|
|
20
|
+
w.pending_json,
|
|
21
|
+
w.pending_since_ms
|
|
22
|
+
FROM wakes w;
|
|
23
|
+
DROP TABLE wakes;
|
|
24
|
+
ALTER TABLE wakes_peer RENAME TO wakes;
|
|
25
|
+
CREATE INDEX IF NOT EXISTS history_peer_ctx ON history(peer, context_id, seq);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- One owner per context id. The first peer to insert the row keeps it (ON CONFLICT DO NOTHING).
|
|
2
|
+
CREATE TABLE conversation_owners (
|
|
3
|
+
context_id TEXT NOT NULL PRIMARY KEY,
|
|
4
|
+
peer TEXT NOT NULL,
|
|
5
|
+
created_at TEXT NOT NULL
|
|
6
|
+
);
|
|
7
|
+
INSERT INTO conversation_owners (context_id, peer, created_at)
|
|
8
|
+
SELECT t.context_id, t.peer, t.created_at
|
|
9
|
+
FROM tasks t
|
|
10
|
+
WHERE t.peer IS NOT NULL AND t.peer != ''
|
|
11
|
+
AND NOT EXISTS (
|
|
12
|
+
SELECT 1 FROM tasks t2
|
|
13
|
+
WHERE t2.context_id = t.context_id AND t2.peer IS NOT NULL AND t2.peer != ''
|
|
14
|
+
AND (t2.created_at < t.created_at OR (t2.created_at = t.created_at AND t2.id < t.id))
|
|
15
|
+
);
|
|
16
|
+
INSERT INTO conversation_owners (context_id, peer, created_at)
|
|
17
|
+
SELECT h.context_id, h.peer, h.ts
|
|
18
|
+
FROM history h
|
|
19
|
+
WHERE h.peer IS NOT NULL AND h.peer != ''
|
|
20
|
+
AND NOT EXISTS (SELECT 1 FROM conversation_owners o WHERE o.context_id = h.context_id)
|
|
21
|
+
AND NOT EXISTS (
|
|
22
|
+
SELECT 1 FROM history h2
|
|
23
|
+
WHERE h2.context_id = h.context_id AND h2.peer IS NOT NULL AND h2.peer != '' AND h2.seq < h.seq
|
|
24
|
+
);
|
|
25
|
+
INSERT INTO conversation_owners (context_id, peer, created_at)
|
|
26
|
+
SELECT o.context_id, o.peer, o.sent_at
|
|
27
|
+
FROM outbound o
|
|
28
|
+
WHERE o.peer IS NOT NULL AND o.peer != ''
|
|
29
|
+
AND NOT EXISTS (SELECT 1 FROM conversation_owners c WHERE c.context_id = o.context_id)
|
|
30
|
+
AND NOT EXISTS (
|
|
31
|
+
SELECT 1 FROM outbound o2
|
|
32
|
+
WHERE o2.context_id = o.context_id AND o2.peer IS NOT NULL AND o2.peer != ''
|
|
33
|
+
AND (o2.sent_at < o.sent_at OR (o2.sent_at = o.sent_at AND o2.task_id < o.task_id))
|
|
34
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- One row per peer. The same context id can be a separate conversation for each peer.
|
|
2
|
+
-- 0011's single-column primary key made a second peer's insert fail, which is how a caller
|
|
3
|
+
-- could tell a fresh id from one another peer already held.
|
|
4
|
+
CREATE TABLE conversation_owners_peer (
|
|
5
|
+
context_id TEXT NOT NULL,
|
|
6
|
+
peer TEXT NOT NULL,
|
|
7
|
+
created_at TEXT NOT NULL,
|
|
8
|
+
PRIMARY KEY (context_id, peer)
|
|
9
|
+
);
|
|
10
|
+
INSERT INTO conversation_owners_peer (context_id, peer, created_at)
|
|
11
|
+
SELECT context_id, peer, created_at FROM conversation_owners;
|
|
12
|
+
DROP TABLE conversation_owners;
|
|
13
|
+
ALTER TABLE conversation_owners_peer RENAME TO conversation_owners;
|
package/worker/src/export.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const EXPORT_TABLES = [
|
|
|
16
16
|
"peers", "tasks", "messages", "history", "outbound", "wakes",
|
|
17
17
|
"rate", "wake_budget", "device_requests", "pairing_rate", "settings",
|
|
18
18
|
"facade_owners", "oauth_clients", "oauth_codes", "mcp_grants",
|
|
19
|
-
"outbound_peers", "oauth_client_metadata", "oidc_txns",
|
|
19
|
+
"outbound_peers", "oauth_client_metadata", "oidc_txns", "conversation_owners",
|
|
20
20
|
] as const;
|
|
21
21
|
|
|
22
22
|
export type ExportTable = (typeof EXPORT_TABLES)[number];
|