instar 1.3.1321 → 1.3.1323
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +20 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +27 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +13 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/middleware.d.ts +13 -0
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +21 -0
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +233 -71
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/A2ADeliveryTracker.d.ts +59 -2
- package/dist/threadline/A2ADeliveryTracker.d.ts.map +1 -1
- package/dist/threadline/A2ADeliveryTracker.js +193 -21
- package/dist/threadline/A2ADeliveryTracker.js.map +1 -1
- package/dist/threadline/ListenerSessionManager.d.ts.map +1 -1
- package/dist/threadline/ListenerSessionManager.js +7 -0
- package/dist/threadline/ListenerSessionManager.js.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.d.ts +9 -0
- package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.js +33 -3
- package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.d.ts +8 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +8 -0
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/client/RelayClient.d.ts.map +1 -1
- package/dist/threadline/client/RelayClient.js +15 -0
- package/dist/threadline/client/RelayClient.js.map +1 -1
- package/dist/threadline/client/ThreadlineClient.d.ts +49 -0
- package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
- package/dist/threadline/client/ThreadlineClient.js +169 -0
- package/dist/threadline/client/ThreadlineClient.js.map +1 -1
- package/dist/threadline/honestDeliveryWiring.d.ts +55 -0
- package/dist/threadline/honestDeliveryWiring.d.ts.map +1 -0
- package/dist/threadline/honestDeliveryWiring.js +142 -0
- package/dist/threadline/honestDeliveryWiring.js.map +1 -0
- package/dist/threadline/mcp-http-client.d.ts.map +1 -1
- package/dist/threadline/mcp-http-client.js +12 -2
- package/dist/threadline/mcp-http-client.js.map +1 -1
- package/dist/threadline/relayConnectionObserver.d.ts.map +1 -1
- package/dist/threadline/relayConnectionObserver.js +10 -3
- package/dist/threadline/relayConnectionObserver.js.map +1 -1
- package/dist/threadline/relayVerdict.d.ts +49 -0
- package/dist/threadline/relayVerdict.d.ts.map +1 -0
- package/dist/threadline/relayVerdict.js +70 -0
- package/dist/threadline/relayVerdict.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/scaffold/templates.ts +13 -0
- package/upgrades/1.3.1322.md +29 -0
- package/upgrades/1.3.1323.md +31 -0
- package/upgrades/a2a-honest-delivery-outcomes.eli16.md +7 -0
- package/upgrades/relay-displaced-reclaim.eli16.md +12 -0
- package/upgrades/side-effects/a2a-honest-delivery-outcomes.md +46 -0
- package/upgrades/side-effects/relay-displaced-reclaim.md +24 -0
package/dist/server/routes.js
CHANGED
|
@@ -130,7 +130,8 @@ import { secretKeyPaths } from '../core/SecretSync.js';
|
|
|
130
130
|
import { PlaywrightProfileRegistry, PlaywrightRegistryError, PlaywrightRegistryCorruptError, DEFAULT_BLOCK_MAX_BYTES as PLAYWRIGHT_BLOCK_MAX_BYTES, } from '../core/PlaywrightProfileRegistry.js';
|
|
131
131
|
import { PlaywrightSeatLease } from '../core/PlaywrightSeatLease.js';
|
|
132
132
|
import { writeConfigAtomic, readSelfKnowledgeFlags } from '../core/BootSelfKnowledge.js';
|
|
133
|
-
import { rateLimiter, signViewPath, OUTBOUND_GATE_REVIEW_BUDGET_MS, resolveFollowMeBudgets, FOLLOWME_SIMPLE_RELAY_FETCH_MS } from './middleware.js';
|
|
133
|
+
import { rateLimiter, signViewPath, OUTBOUND_GATE_REVIEW_BUDGET_MS, resolveFollowMeBudgets, FOLLOWME_SIMPLE_RELAY_FETCH_MS, bearerMatches } from './middleware.js';
|
|
134
|
+
import { relayHoldHours } from '../threadline/relayVerdict.js';
|
|
134
135
|
import { buildTopicProfileOptions, validateDashboardProfileChoice, seedTopicProfileAtCreation, claimDashboardCreatedTopic, settleDashboardCreatedTopic, frameworkLabel } from '../core/dashboardTopicProfile.js';
|
|
135
136
|
import { reviewWithinBudget } from './outboundGateBudget.js';
|
|
136
137
|
import { resolveToneRecipientClass } from './toneRecipientClass.js';
|
|
@@ -3821,16 +3822,9 @@ export function createRoutes(ctx) {
|
|
|
3821
3822
|
};
|
|
3822
3823
|
// Include detailed info only for authenticated callers.
|
|
3823
3824
|
// Must actually validate the token here since authMiddleware skips /health.
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
if (header?.startsWith('Bearer ')) {
|
|
3828
|
-
const token = header.slice(7);
|
|
3829
|
-
const ha = createHash('sha256').update(token).digest();
|
|
3830
|
-
const hb = createHash('sha256').update(ctx.config.authToken).digest();
|
|
3831
|
-
isAuthed = timingSafeEqual(ha, hb);
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3825
|
+
// Token-only compare, shared with the pool-scope peer-health read
|
|
3826
|
+
// (`bearerMatches`): byte-identical to the inline check it replaced.
|
|
3827
|
+
const isAuthed = bearerMatches(req, ctx.config.authToken);
|
|
3834
3828
|
if (isAuthed) {
|
|
3835
3829
|
const mem = process.memoryUsage();
|
|
3836
3830
|
// Use ProcessIntegrity for truthful version reporting
|
|
@@ -3871,6 +3865,14 @@ export function createRoutes(ctx) {
|
|
|
3871
3865
|
base.telegramOriginStorage = { state: 'unavailable' };
|
|
3872
3866
|
}
|
|
3873
3867
|
}
|
|
3868
|
+
// Honest delivery (spec §1): verdict-dispatcher counters ride the AUTHED
|
|
3869
|
+
// branch only — every /threadline/* path bypasses Bearer auth by design.
|
|
3870
|
+
if (ctx.relayVerdictCounters) {
|
|
3871
|
+
try {
|
|
3872
|
+
base.threadline = { ...(base.threadline ?? {}), relayVerdicts: ctx.relayVerdictCounters() };
|
|
3873
|
+
}
|
|
3874
|
+
catch { /* counters are best-effort observability */ }
|
|
3875
|
+
}
|
|
3874
3876
|
base.project = ctx.config.projectName;
|
|
3875
3877
|
base.node = process.version;
|
|
3876
3878
|
base.memory = {
|
|
@@ -16430,26 +16432,113 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
16430
16432
|
// ── A2A peer-health (A2A-DURABLE-DELIVERY-SPEC.md) ──────────────────────
|
|
16431
16433
|
// "Is my channel to <peer> alive?" as a lookup, not a guess. Read-only
|
|
16432
16434
|
// observability over the durable delivery tracker — never gates a send.
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
+
// Security posture (spec §6): the DEFAULT scope is reachable without a token
|
|
16436
|
+
// (pre-existing `/threadline/*` middleware exemption), per peer, this machine
|
|
16437
|
+
// only. `?scope=pool` fans out to every machine and therefore checks the token
|
|
16438
|
+
// inline (token-only compare + the agent-id rule) — it guards AMPLIFICATION,
|
|
16439
|
+
// not the per-peer data. Peers are queried with the default scope only.
|
|
16440
|
+
const peerHealthInstarVersion = () => {
|
|
16441
|
+
const integrity = ProcessIntegrity.getInstance();
|
|
16442
|
+
return integrity?.runningVersion ?? (ctx.config.version || '0.0.0');
|
|
16443
|
+
};
|
|
16444
|
+
const poolScopeAuthorized = (req, res) => {
|
|
16445
|
+
if (!bearerMatches(req, ctx.config.authToken)) {
|
|
16446
|
+
res.status(403).json({ error: 'forbidden', scope: 'pool' });
|
|
16447
|
+
return false;
|
|
16448
|
+
}
|
|
16449
|
+
const provided = req.headers['x-instar-agentid'];
|
|
16450
|
+
if (typeof provided === 'string' && ctx.config.projectName && provided !== ctx.config.projectName) {
|
|
16451
|
+
res.status(403).json({ error: 'agent_id_mismatch', expected: ctx.config.projectName });
|
|
16452
|
+
return false;
|
|
16453
|
+
}
|
|
16454
|
+
return true;
|
|
16455
|
+
};
|
|
16456
|
+
const isPeerHealthBody = (b) => Array.isArray(b.peers) && typeof b.count === 'number';
|
|
16457
|
+
const isPeerHealthOne = (b) => typeof b.peerFp === 'string' && typeof b.stale === 'boolean';
|
|
16458
|
+
const poolPeerRows = (peers, pick) => {
|
|
16459
|
+
const local = peerHealthInstarVersion();
|
|
16460
|
+
let mixedVersion = false;
|
|
16461
|
+
const rows = [];
|
|
16462
|
+
for (const p of peers) {
|
|
16463
|
+
const v = typeof p.body.instarVersion === 'string' ? p.body.instarVersion : null;
|
|
16464
|
+
if (v !== local)
|
|
16465
|
+
mixedVersion = true; // absent IS the mixed case
|
|
16466
|
+
for (const item of pick(p.body)) {
|
|
16467
|
+
rows.push({ ...item, machineId: p.machineId, machineNickname: p.nickname, instarVersion: v });
|
|
16468
|
+
}
|
|
16469
|
+
}
|
|
16470
|
+
return { rows, mixedVersion };
|
|
16471
|
+
};
|
|
16472
|
+
router.get('/threadline/peers/health', async (req, res) => {
|
|
16473
|
+
if (!ctx.a2aDeliveryTracker && req.query.scope !== 'pool') {
|
|
16435
16474
|
res.status(503).json({ error: 'A2A delivery tracker not initialized' });
|
|
16436
16475
|
return;
|
|
16437
16476
|
}
|
|
16438
16477
|
const staleAfterMs = typeof req.query.staleAfterMs === 'string' && Number.isFinite(Number(req.query.staleAfterMs))
|
|
16439
16478
|
? Number(req.query.staleAfterMs)
|
|
16440
16479
|
: undefined;
|
|
16441
|
-
const peers = ctx.a2aDeliveryTracker
|
|
16442
|
-
|
|
16480
|
+
const peers = ctx.a2aDeliveryTracker?.allPeerHealth({ staleAfterMs }) ?? null;
|
|
16481
|
+
const localBody = peers
|
|
16482
|
+
? { peers, count: peers.length, staleCount: peers.filter((p) => p.stale).length, instarVersion: peerHealthInstarVersion() }
|
|
16483
|
+
: null;
|
|
16484
|
+
if (req.query.scope !== 'pool') {
|
|
16485
|
+
if (!localBody) {
|
|
16486
|
+
res.status(503).json({ error: 'A2A delivery tracker not initialized' });
|
|
16487
|
+
return;
|
|
16488
|
+
}
|
|
16489
|
+
res.json(localBody);
|
|
16490
|
+
return;
|
|
16491
|
+
}
|
|
16492
|
+
if (!poolScopeAuthorized(req, res))
|
|
16493
|
+
return;
|
|
16494
|
+
const qs = staleAfterMs !== undefined ? `?staleAfterMs=${staleAfterMs}` : '';
|
|
16495
|
+
const { peers: pool, failed, peersQueried } = await readReaperPoolPeers(`/threadline/peers/health${qs}`, isPeerHealthBody);
|
|
16496
|
+
const selfId = ctx.meshSelfId ?? 'local';
|
|
16497
|
+
const { rows, mixedVersion } = poolPeerRows(pool, (b) => b.peers);
|
|
16498
|
+
const localRows = (localBody?.peers ?? []).map((p) => ({ ...p, machineId: selfId, machineNickname: null, instarVersion: localBody?.instarVersion ?? null }));
|
|
16499
|
+
const all = [...localRows, ...rows];
|
|
16500
|
+
res.json({
|
|
16501
|
+
scope: 'pool',
|
|
16502
|
+
local: localBody,
|
|
16503
|
+
peers: all,
|
|
16504
|
+
count: all.length,
|
|
16505
|
+
staleCount: all.filter((p) => p.stale).length,
|
|
16506
|
+
mixedVersion,
|
|
16507
|
+
pool: { selfMachineId: selfId, peersQueried, peersOk: pool.length, failed },
|
|
16508
|
+
});
|
|
16443
16509
|
});
|
|
16444
|
-
router.get('/threadline/peers/:fp/health', (req, res) => {
|
|
16445
|
-
|
|
16446
|
-
|
|
16510
|
+
router.get('/threadline/peers/:fp/health', async (req, res) => {
|
|
16511
|
+
const fp = req.params.fp;
|
|
16512
|
+
if (!/^[0-9a-f]{6,64}$/i.test(fp)) {
|
|
16513
|
+
res.status(400).json({ error: 'invalid fingerprint' });
|
|
16447
16514
|
return;
|
|
16448
16515
|
}
|
|
16449
16516
|
const staleAfterMs = typeof req.query.staleAfterMs === 'string' && Number.isFinite(Number(req.query.staleAfterMs))
|
|
16450
16517
|
? Number(req.query.staleAfterMs)
|
|
16451
16518
|
: undefined;
|
|
16452
|
-
|
|
16519
|
+
const local = ctx.a2aDeliveryTracker
|
|
16520
|
+
? { ...ctx.a2aDeliveryTracker.peerHealth(fp, { staleAfterMs }), instarVersion: peerHealthInstarVersion() }
|
|
16521
|
+
: null;
|
|
16522
|
+
if (req.query.scope !== 'pool') {
|
|
16523
|
+
if (!local) {
|
|
16524
|
+
res.status(503).json({ error: 'A2A delivery tracker not initialized' });
|
|
16525
|
+
return;
|
|
16526
|
+
}
|
|
16527
|
+
res.json(local);
|
|
16528
|
+
return;
|
|
16529
|
+
}
|
|
16530
|
+
if (!poolScopeAuthorized(req, res))
|
|
16531
|
+
return;
|
|
16532
|
+
const qs = staleAfterMs !== undefined ? `?staleAfterMs=${staleAfterMs}` : '';
|
|
16533
|
+
const { peers: pool, failed, peersQueried } = await readReaperPoolPeers(`/threadline/peers/${encodeURIComponent(fp)}/health${qs}`, isPeerHealthOne);
|
|
16534
|
+
const selfId = ctx.meshSelfId ?? 'local';
|
|
16535
|
+
const { rows, mixedVersion } = poolPeerRows(pool, (b) => [b]);
|
|
16536
|
+
const all = [...(local ? [{ ...local, machineId: selfId, machineNickname: null }] : []), ...rows];
|
|
16537
|
+
res.json({
|
|
16538
|
+
scope: 'pool', peerFp: fp, local, machines: all,
|
|
16539
|
+
staleCount: all.filter((p) => p.stale).length,
|
|
16540
|
+
mixedVersion, pool: { selfMachineId: selfId, peersQueried, peersOk: pool.length, failed },
|
|
16541
|
+
});
|
|
16453
16542
|
});
|
|
16454
16543
|
// ── Secure A2A Verified Pairing surfaces (§3.6) ────────────────────────
|
|
16455
16544
|
// List + detail + the PIN-gated operator verify. Ships DARK behind
|
|
@@ -34100,7 +34189,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
34100
34189
|
// ── Threadline Reply Waiter ─────────────────────────────────────────
|
|
34101
34190
|
// Waits for an incoming reply from a specific agent on a specific thread.
|
|
34102
34191
|
// Used by the relay-send endpoint when waitForReply is true.
|
|
34103
|
-
function waitForThreadlineReply(routeCtx, senderAgent, threadId, timeoutSec) {
|
|
34192
|
+
function waitForThreadlineReply(routeCtx, senderAgent, threadId, timeoutSec, onRegistered) {
|
|
34104
34193
|
const timeout = Math.min(Math.max(timeoutSec ?? 120, 5), 300) * 1000; // 5s–300s, default 120s
|
|
34105
34194
|
// PR-3: Waiters are keyed by threadId (unique per conversation) rather
|
|
34106
34195
|
// than sender agent name (which can collide when multiple agents share
|
|
@@ -34110,7 +34199,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
34110
34199
|
routeCtx.threadlineReplyWaiters.delete(threadId);
|
|
34111
34200
|
resolve(null);
|
|
34112
34201
|
}, timeout);
|
|
34113
|
-
|
|
34202
|
+
const entry = {
|
|
34114
34203
|
resolve: (reply) => {
|
|
34115
34204
|
clearTimeout(timer);
|
|
34116
34205
|
routeCtx.threadlineReplyWaiters.delete(threadId);
|
|
@@ -34119,6 +34208,16 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
34119
34208
|
threadId,
|
|
34120
34209
|
senderAgent,
|
|
34121
34210
|
timer,
|
|
34211
|
+
};
|
|
34212
|
+
routeCtx.threadlineReplyWaiters.set(threadId, entry);
|
|
34213
|
+
// Honest delivery (spec §4): a refused send cancels ITS OWN waiter only —
|
|
34214
|
+
// an orphaned waiter's timeout would otherwise delete a retry's waiter on
|
|
34215
|
+
// the same thread.
|
|
34216
|
+
onRegistered?.(() => {
|
|
34217
|
+
clearTimeout(timer);
|
|
34218
|
+
if (routeCtx.threadlineReplyWaiters.get(threadId) === entry)
|
|
34219
|
+
routeCtx.threadlineReplyWaiters.delete(threadId);
|
|
34220
|
+
resolve(null);
|
|
34122
34221
|
});
|
|
34123
34222
|
});
|
|
34124
34223
|
}
|
|
@@ -34858,8 +34957,73 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
34858
34957
|
// The peer replies on the thread that went on the wire, so record, return
|
|
34859
34958
|
// and wait on THAT id — never the messageId (ACT-1304 fault 3).
|
|
34860
34959
|
const { messageId: relayMsgId, threadId: effectiveRelayThreadId } = relayClient.sendAutoWithThread(resolvedId, message, threadId);
|
|
34861
|
-
//
|
|
34862
|
-
//
|
|
34960
|
+
// waitForReply: the reply listener is registered NOW, before any await, so
|
|
34961
|
+
// it runs concurrently with the verdict wait (no time comes off the reply
|
|
34962
|
+
// budget, and a fast reply can never land in a gap with no listener).
|
|
34963
|
+
let cancelReplyWait;
|
|
34964
|
+
const replyPromise = waitForReply
|
|
34965
|
+
? waitForThreadlineReply(ctx, resolvedId, effectiveRelayThreadId, timeoutSeconds, (c) => { cancelReplyWait = c; })
|
|
34966
|
+
: null;
|
|
34967
|
+
// Honest delivery (docs/specs/a2a-honest-delivery-outcomes.md §4). Order:
|
|
34968
|
+
// 1. send (above) 2. recordSent + canonical thread leg + captureOrigin,
|
|
34969
|
+
// all BEFORE the verdict wait so a fast reply never precedes them —
|
|
34970
|
+
// 3. wait ≤3 s for the relay's verdict (cache covers a verdict that
|
|
34971
|
+
// landed during captureOrigin) 4. outbox ONCE with the final outcome
|
|
34972
|
+
// 5. bridge mirror + reply-claim release on 2xx; a refusal 502s and the
|
|
34973
|
+
// finish handler releases the claim (retry allowed).
|
|
34974
|
+
try {
|
|
34975
|
+
ctx.a2aDeliveryTracker?.recordSent({
|
|
34976
|
+
messageId: relayMsgId,
|
|
34977
|
+
peerFp: resolvedId,
|
|
34978
|
+
peerName: targetAgent,
|
|
34979
|
+
threadId: effectiveRelayThreadId,
|
|
34980
|
+
transport: 'relay',
|
|
34981
|
+
});
|
|
34982
|
+
}
|
|
34983
|
+
catch (err) {
|
|
34984
|
+
// @silent-fallback-ok: recording-only — A2A delivery tracking must never break
|
|
34985
|
+
// the send (the message was already submitted above). Logged.
|
|
34986
|
+
console.warn(`[relay-send] A2A delivery record failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
34987
|
+
}
|
|
34988
|
+
// Robustness Phase 2 (D-B): append the relay-submitted outbound leg to the
|
|
34989
|
+
// canonical log through the funnel. The wire createdAt is stamped inside the
|
|
34990
|
+
// relay client, so this end's createdAt is best-effort (symmetry on the relay
|
|
34991
|
+
// path degrades to unverified, advisory-only); F3 holds unconditionally — the
|
|
34992
|
+
// sender's own message is now auditable per-thread regardless of symmetry.
|
|
34993
|
+
// Known limit: a leg the relay then REFUSES stays in the log (the verdict is
|
|
34994
|
+
// not known yet), so that thread reads `diverged` until the next exchange.
|
|
34995
|
+
if (ctx.threadMessageRecorder) {
|
|
34996
|
+
recordThreadMessage(ctx.threadMessageRecorder, {
|
|
34997
|
+
threadId: effectiveRelayThreadId,
|
|
34998
|
+
messageId: relayMsgId,
|
|
34999
|
+
direction: 'outbound',
|
|
35000
|
+
body: message,
|
|
35001
|
+
createdAt: new Date().toISOString(),
|
|
35002
|
+
peerFingerprint: resolvedId ?? undefined,
|
|
35003
|
+
author: { machineId: ctx.meshSelfId ?? undefined, sessionName: typeof originSessionName === 'string' ? originSessionName : undefined },
|
|
35004
|
+
subject: typeof purpose === 'string' ? purpose : undefined,
|
|
35005
|
+
});
|
|
35006
|
+
}
|
|
35007
|
+
// `resolvedId` is the peer's full routing fingerprint; store IT as the
|
|
35008
|
+
// canonical thread owner (display = the raw target the caller typed) so a
|
|
35009
|
+
// reply matches the anti-hijack guard instead of being false-isolated.
|
|
35010
|
+
await captureOrigin(effectiveRelayThreadId, resolvedId, targetAgent);
|
|
35011
|
+
const RELAY_ACK_WAIT_MS = 3000;
|
|
35012
|
+
const verdict = await relayClient.awaitRelayAck(relayMsgId, RELAY_ACK_WAIT_MS);
|
|
35013
|
+
if (verdict?.status === 'rejected')
|
|
35014
|
+
cancelReplyWait?.();
|
|
35015
|
+
const relayStatus = verdict?.status ?? 'unconfirmed';
|
|
35016
|
+
const relayReasonCode = verdict?.reasonCode ?? (relayStatus === 'unconfirmed' && relayClient.banSuspected ? 'banned' : undefined);
|
|
35017
|
+
const banSuspected = relayStatus === 'unconfirmed' && relayClient.banSuspected;
|
|
35018
|
+
const outboxOutcome = relayStatus === 'delivered' ? 'relay-sent'
|
|
35019
|
+
: relayStatus === 'queued' ? 'relay-queued'
|
|
35020
|
+
: relayStatus === 'rejected' ? 'relay-rejected'
|
|
35021
|
+
: 'relay-unconfirmed';
|
|
35022
|
+
if (outboxOutcome === 'relay-unconfirmed')
|
|
35023
|
+
relayClient.noteUnconfirmedSettled();
|
|
35024
|
+
// Canonical outbox write for the relay-delivery path — ONCE, with the final
|
|
35025
|
+
// outcome (the outbox is append-only + HMAC-per-line; no placeholder, no
|
|
35026
|
+
// update-by-id). Same shape as the local-delivery path above.
|
|
34863
35027
|
if (ctx.listenerManager) {
|
|
34864
35028
|
try {
|
|
34865
35029
|
ctx.listenerManager.appendCanonicalOutboxEntry({
|
|
@@ -34871,35 +35035,40 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
34871
35035
|
text: message,
|
|
34872
35036
|
messageId: relayMsgId,
|
|
34873
35037
|
inReplyTo: typeof inReplyTo === 'string' ? inReplyTo : undefined,
|
|
34874
|
-
outcome:
|
|
35038
|
+
outcome: outboxOutcome,
|
|
34875
35039
|
});
|
|
34876
|
-
if (typeof inReplyTo === 'string')
|
|
34877
|
-
ctx.listenerManager.releaseReplyClaim(inReplyTo, replyClaimOwner);
|
|
34878
35040
|
}
|
|
34879
35041
|
catch (err) {
|
|
34880
35042
|
console.warn(`[relay-send] Canonical outbox append failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
34881
|
-
if (typeof inReplyTo === 'string')
|
|
35043
|
+
if (typeof inReplyTo === 'string' && relayStatus !== 'rejected')
|
|
34882
35044
|
ctx.listenerManager.retainReplyClaimFailure(inReplyTo, replyClaimOwner);
|
|
34883
35045
|
}
|
|
34884
35046
|
}
|
|
34885
|
-
|
|
34886
|
-
|
|
34887
|
-
|
|
34888
|
-
|
|
34889
|
-
|
|
34890
|
-
|
|
34891
|
-
|
|
34892
|
-
|
|
35047
|
+
if (relayStatus === 'rejected') {
|
|
35048
|
+
// A refused send is a failed send: 502 (the relay is the upstream), the
|
|
35049
|
+
// body carries the contract (`retryLater`, never the relay's prose), and
|
|
35050
|
+
// the >=400 finish handler releases the reply claim so a retry is allowed.
|
|
35051
|
+
// NOT retainReplyClaimFailure — that marks "delivered but settlement
|
|
35052
|
+
// failed" and would block the retry forever.
|
|
35053
|
+
res.status(502).json({
|
|
35054
|
+
success: false,
|
|
35055
|
+
error: `not delivered: relay refused (${relayReasonCode ?? 'unmapped'}). Tracked; do not resend now.`,
|
|
35056
|
+
accepted: false,
|
|
35057
|
+
delivered: false,
|
|
34893
35058
|
messageId: relayMsgId,
|
|
34894
|
-
|
|
34895
|
-
|
|
34896
|
-
|
|
34897
|
-
|
|
34898
|
-
|
|
34899
|
-
|
|
35059
|
+
threadId: effectiveRelayThreadId,
|
|
35060
|
+
resolvedAgent: resolvedId,
|
|
35061
|
+
deliveryPath: 'relay',
|
|
35062
|
+
relayStatus: 'rejected',
|
|
35063
|
+
relayReasonCode: relayReasonCode ?? 'unmapped',
|
|
35064
|
+
retryLater: verdict?.retryLater ?? null,
|
|
35065
|
+
deliveryOutcome: `not delivered: relay refused (${relayReasonCode ?? 'unmapped'}). Tracked; do not resend now.`,
|
|
35066
|
+
topicLinkageStamped: resolvedOriginTopicId !== undefined,
|
|
34900
35067
|
});
|
|
35068
|
+
return;
|
|
34901
35069
|
}
|
|
34902
|
-
// Mirror outbound into Telegram bridge (relay-only — best effort)
|
|
35070
|
+
// Mirror outbound into Telegram bridge (relay-only — best effort), then
|
|
35071
|
+
// release the reply claim: this is the 2xx path.
|
|
34903
35072
|
if (ctx.telegramBridge) {
|
|
34904
35073
|
ctx.telegramBridge.mirrorOutbound({
|
|
34905
35074
|
threadId: effectiveRelayThreadId,
|
|
@@ -34907,43 +35076,35 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
34907
35076
|
remoteAgentName: targetAgent,
|
|
34908
35077
|
text: message,
|
|
34909
35078
|
messageId: relayMsgId,
|
|
34910
|
-
outcome:
|
|
35079
|
+
outcome: outboxOutcome,
|
|
34911
35080
|
}).catch(() => { });
|
|
34912
35081
|
}
|
|
34913
|
-
|
|
34914
|
-
|
|
34915
|
-
|
|
34916
|
-
|
|
34917
|
-
|
|
34918
|
-
|
|
34919
|
-
|
|
34920
|
-
|
|
34921
|
-
|
|
34922
|
-
|
|
34923
|
-
|
|
34924
|
-
|
|
34925
|
-
|
|
34926
|
-
|
|
34927
|
-
threadId: effectiveRelayThreadId,
|
|
34928
|
-
transport: 'relay',
|
|
34929
|
-
});
|
|
34930
|
-
}
|
|
34931
|
-
catch (err) {
|
|
34932
|
-
// @silent-fallback-ok: recording-only — A2A delivery tracking must never break
|
|
34933
|
-
// the send (the message was already submitted above). Logged.
|
|
34934
|
-
console.warn(`[relay-send] A2A delivery record failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
34935
|
-
}
|
|
35082
|
+
if (ctx.listenerManager && typeof inReplyTo === 'string')
|
|
35083
|
+
ctx.listenerManager.releaseReplyClaim(inReplyTo, replyClaimOwner);
|
|
35084
|
+
const deliveryOutcome = relayStatus === 'delivered'
|
|
35085
|
+
? "handed to the peer's relay connection; not yet confirmed read"
|
|
35086
|
+
: relayStatus === 'queued'
|
|
35087
|
+
? `peer offline; the relay holds it for up to ${relayHoldHours(verdict?.ttlSec)} h`
|
|
35088
|
+
: banSuspected
|
|
35089
|
+
? 'submitted to relay; the relay reports this sender banned'
|
|
35090
|
+
: `submitted to relay; no relay acknowledgement within ${RELAY_ACK_WAIT_MS / 1000}s`;
|
|
35091
|
+
const honest = {
|
|
35092
|
+
relayStatus,
|
|
35093
|
+
...(relayReasonCode ? { relayReasonCode } : {}),
|
|
35094
|
+
...(banSuspected ? { banSuspected: true } : {}),
|
|
35095
|
+
};
|
|
34936
35096
|
if (waitForReply) {
|
|
34937
|
-
const reply = await
|
|
35097
|
+
const reply = await replyPromise;
|
|
34938
35098
|
res.json({
|
|
34939
35099
|
success: true,
|
|
34940
|
-
accepted: reply !== null,
|
|
35100
|
+
accepted: reply !== null || relayStatus === 'delivered' || relayStatus === 'queued',
|
|
34941
35101
|
delivered: reply !== null,
|
|
34942
35102
|
messageId: relayMsgId,
|
|
34943
35103
|
threadId: effectiveRelayThreadId,
|
|
34944
35104
|
resolvedAgent: resolvedId,
|
|
34945
35105
|
deliveryPath: 'relay',
|
|
34946
|
-
deliveryOutcome: reply !== null ? 'reply received' :
|
|
35106
|
+
deliveryOutcome: reply !== null ? 'reply received' : deliveryOutcome,
|
|
35107
|
+
...honest,
|
|
34947
35108
|
reply,
|
|
34948
35109
|
topicLinkageStamped: resolvedOriginTopicId !== undefined,
|
|
34949
35110
|
});
|
|
@@ -34951,13 +35112,14 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
34951
35112
|
else {
|
|
34952
35113
|
res.json({
|
|
34953
35114
|
success: true,
|
|
34954
|
-
accepted:
|
|
35115
|
+
accepted: relayStatus === 'delivered' || relayStatus === 'queued',
|
|
34955
35116
|
delivered: false,
|
|
34956
35117
|
messageId: relayMsgId,
|
|
34957
35118
|
threadId: effectiveRelayThreadId,
|
|
34958
35119
|
resolvedAgent: resolvedId,
|
|
34959
35120
|
deliveryPath: 'relay',
|
|
34960
|
-
deliveryOutcome
|
|
35121
|
+
deliveryOutcome,
|
|
35122
|
+
...honest,
|
|
34961
35123
|
topicLinkageStamped: resolvedOriginTopicId !== undefined,
|
|
34962
35124
|
});
|
|
34963
35125
|
}
|