instar 1.3.518 → 1.3.520
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/dashboard/index.html +40 -10
- package/dashboard/mandates.js +115 -34
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +26 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +29 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +4 -0
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +29 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -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 +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +310 -7
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ConversationStore.d.ts +90 -0
- package/dist/threadline/ConversationStore.d.ts.map +1 -1
- package/dist/threadline/ConversationStore.js +133 -0
- package/dist/threadline/ConversationStore.js.map +1 -1
- package/dist/threadline/ThreadLog.d.ts +200 -0
- package/dist/threadline/ThreadLog.d.ts.map +1 -0
- package/dist/threadline/ThreadLog.js +434 -0
- package/dist/threadline/ThreadLog.js.map +1 -0
- package/dist/threadline/ThreadlineEndpoints.d.ts +10 -1
- package/dist/threadline/ThreadlineEndpoints.d.ts.map +1 -1
- package/dist/threadline/ThreadlineEndpoints.js +63 -12
- package/dist/threadline/ThreadlineEndpoints.js.map +1 -1
- package/dist/threadline/canonicalHistoryRead.d.ts +63 -0
- package/dist/threadline/canonicalHistoryRead.d.ts.map +1 -0
- package/dist/threadline/canonicalHistoryRead.js +123 -0
- package/dist/threadline/canonicalHistoryRead.js.map +1 -0
- package/dist/threadline/recordThreadMessage.d.ts +143 -0
- package/dist/threadline/recordThreadMessage.d.ts.map +1 -0
- package/dist/threadline/recordThreadMessage.js +222 -0
- package/dist/threadline/recordThreadMessage.js.map +1 -0
- package/dist/threadline/threadDigest.d.ts +90 -0
- package/dist/threadline/threadDigest.d.ts.map +1 -0
- package/dist/threadline/threadDigest.js +130 -0
- package/dist/threadline/threadDigest.js.map +1 -0
- package/dist/threadline/threadSymmetry.d.ts +108 -0
- package/dist/threadline/threadSymmetry.d.ts.map +1 -0
- package/dist/threadline/threadSymmetry.js +225 -0
- package/dist/threadline/threadSymmetry.js.map +1 -0
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +46 -0
- package/scripts/lib/operator-surface.mjs +54 -0
- package/skills/instar-dev/templates/side-effects-artifact.md +15 -0
- package/src/data/builtin-manifest.json +65 -65
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.3.519.md +92 -0
- package/upgrades/1.3.520.md +50 -0
- package/upgrades/side-effects/operator-surface-quality-and-mandates-redesign.md +200 -0
- package/upgrades/side-effects/threadline-canonical-history.md +122 -0
package/dist/server/routes.js
CHANGED
|
@@ -129,6 +129,10 @@ import { evaluateAndRecordInbound } from '../threadline/WarrantsReplyGate.js';
|
|
|
129
129
|
import { createThreadlineRoutes } from '../threadline/ThreadlineEndpoints.js';
|
|
130
130
|
import { evaluateSendGate, negotiatorLogDir } from '../threadline/NegotiatorGate.js';
|
|
131
131
|
import { recordInboundAck } from '../threadline/recordInboundAck.js';
|
|
132
|
+
import { recordThreadMessage } from '../threadline/recordThreadMessage.js';
|
|
133
|
+
import { THREAD_ID_RE } from '../threadline/ThreadLog.js';
|
|
134
|
+
import { readThreadHistoryUnion } from '../threadline/canonicalHistoryRead.js';
|
|
135
|
+
import { computeSymmetryState, localThreadSync, honorPeerThreadSync } from '../threadline/threadSymmetry.js';
|
|
132
136
|
import { resolveSingleNegotiatorConfig, readNegotiatorCounts } from '../threadline/NegotiatorLease.js';
|
|
133
137
|
import { detectCommitmentClass, commitmentNudge } from '../threadline/ContentClassifier.js';
|
|
134
138
|
import { DEFAULT_RELAY_URL } from '../threadline/constants.js';
|
|
@@ -8681,7 +8685,29 @@ export function createRoutes(ctx) {
|
|
|
8681
8685
|
res.status(404).json({ error: 'Thread not found' });
|
|
8682
8686
|
return;
|
|
8683
8687
|
}
|
|
8684
|
-
|
|
8688
|
+
// Robustness Phase 2 (E6): surface the CANONICAL log head + advisory symmetry
|
|
8689
|
+
// state onto the dashboard thread view, so corrected history + divergence are
|
|
8690
|
+
// visible to the human (the canonical log is the authoritative count, not the
|
|
8691
|
+
// lossy aggregate). Additive — absent when threadline/canonical-history is off.
|
|
8692
|
+
let canonicalHistory;
|
|
8693
|
+
try {
|
|
8694
|
+
const tid = req.params.threadId;
|
|
8695
|
+
if (ctx.threadLog && THREAD_ID_RE.test(tid) && ctx.threadLog.isPathConfined(tid)) {
|
|
8696
|
+
const head = ctx.threadLog.head(tid);
|
|
8697
|
+
const verify = ctx.threadLog.verify(tid);
|
|
8698
|
+
const conv = ctx.conversationStore?.get(tid);
|
|
8699
|
+
canonicalHistory = {
|
|
8700
|
+
count: head.count,
|
|
8701
|
+
headHash: head.headHash,
|
|
8702
|
+
setAccum: head.setAccum,
|
|
8703
|
+
chainOk: verify.ok,
|
|
8704
|
+
symmetryState: conv?.symmetryState ?? 'unknown',
|
|
8705
|
+
collisionCount: conv?.collisionCount ?? 0,
|
|
8706
|
+
};
|
|
8707
|
+
}
|
|
8708
|
+
}
|
|
8709
|
+
catch { /* @silent-fallback-ok: observability augmentation is best-effort; the base detail still returns */ }
|
|
8710
|
+
res.json(canonicalHistory ? { ...detail, canonicalHistory } : detail);
|
|
8685
8711
|
});
|
|
8686
8712
|
// ── A2A peer-health (A2A-DURABLE-DELIVERY-SPEC.md) ──────────────────────
|
|
8687
8713
|
// "Is my channel to <peer> alive?" as a lookup, not a guess. Read-only
|
|
@@ -17507,6 +17533,33 @@ export function createRoutes(ctx) {
|
|
|
17507
17533
|
senderFingerprint: senderAgent ?? undefined,
|
|
17508
17534
|
senderName: senderAgent ?? null,
|
|
17509
17535
|
});
|
|
17536
|
+
// Robustness Phase 2 (D-B): append THIS inbound leg to the canonical log
|
|
17537
|
+
// through the single funnel (Structure > Willpower — the wiring-integrity
|
|
17538
|
+
// test asserts every message-persisting route goes through it). createdAt is
|
|
17539
|
+
// hashed AS RECEIVED, so the digest matches the sender's on the co-located
|
|
17540
|
+
// path. The peer fingerprint is the thread owner (NOT the asserted name).
|
|
17541
|
+
if (ctx.threadMessageRecorder && envelope.message?.threadId && envelope.message?.id) {
|
|
17542
|
+
const rtmBody = typeof envelope.message?.body === 'string'
|
|
17543
|
+
? envelope.message.body
|
|
17544
|
+
: String(envelope.message?.body?.content ?? envelope.message?.body?.text ?? '');
|
|
17545
|
+
const ownerFp = ctx.threadResumeMap?.get(envelope.message.threadId)?.remoteAgent;
|
|
17546
|
+
recordThreadMessage(ctx.threadMessageRecorder, {
|
|
17547
|
+
threadId: envelope.message.threadId,
|
|
17548
|
+
messageId: envelope.message.id,
|
|
17549
|
+
direction: 'inbound',
|
|
17550
|
+
body: rtmBody,
|
|
17551
|
+
createdAt: typeof envelope.message?.createdAt === 'string' ? envelope.message.createdAt : new Date().toISOString(),
|
|
17552
|
+
peerFingerprint: ownerFp || (senderAgent ?? undefined),
|
|
17553
|
+
author: { agentFingerprint: ownerFp || (senderAgent ?? undefined) },
|
|
17554
|
+
subject: typeof envelope.message?.subject === 'string' ? envelope.message.subject : undefined,
|
|
17555
|
+
});
|
|
17556
|
+
// Honor a piggybacked peer threadSync on the co-located path (D-D). The
|
|
17557
|
+
// verified participant key is the thread OWNER fingerprint (captured by
|
|
17558
|
+
// captureOrigin), never the asserted sender name. Advisory-only.
|
|
17559
|
+
if (ownerFp && envelope.threadSync && ctx.threadLog && ctx.conversationStore) {
|
|
17560
|
+
void honorPeerThreadSync({ threadLog: ctx.threadLog, conversationStore: ctx.conversationStore, attention: ctx.telegram ?? null }, envelope.message.threadId, ownerFp, envelope.threadSync).catch(() => { });
|
|
17561
|
+
}
|
|
17562
|
+
}
|
|
17510
17563
|
// Check if this message resolves a pending waitForReply request.
|
|
17511
17564
|
// Local delivery bypasses the relay client's gate-passed event, so we
|
|
17512
17565
|
// must check reply waiters here directly.
|
|
@@ -17739,17 +17792,76 @@ export function createRoutes(ctx) {
|
|
|
17739
17792
|
res.status(500).json({ error: err instanceof Error ? err.message : 'Thread list failed' });
|
|
17740
17793
|
}
|
|
17741
17794
|
});
|
|
17795
|
+
// Robustness Phase 2 (D-C): build the UNION read deps (canonical log ∪ bounded
|
|
17796
|
+
// memoized backfill). The outbox supplies outbound legs (tail-bounded); the
|
|
17797
|
+
// derived aggregate supplies inbound legs (O(thread), never a full-store scan).
|
|
17798
|
+
const buildCanonicalReadDeps = () => {
|
|
17799
|
+
if (!ctx.threadLog || !ctx.threadMessageRecorder)
|
|
17800
|
+
return null;
|
|
17801
|
+
return {
|
|
17802
|
+
threadLog: ctx.threadLog,
|
|
17803
|
+
threadMessageRecorder: ctx.threadMessageRecorder,
|
|
17804
|
+
conversationStore: ctx.conversationStore,
|
|
17805
|
+
outboxPath: ctx.listenerManager?.canonicalOutboxPath
|
|
17806
|
+
?? path.join(ctx.config.stateDir, 'threadline', 'outbox.jsonl.active'),
|
|
17807
|
+
selfName: ctx.config.projectName,
|
|
17808
|
+
backfillOutboxTailLines: ctx.config.threadline?.canonicalHistory?.backfillOutboxTailLines,
|
|
17809
|
+
getAggregateMessages: async (tid) => {
|
|
17810
|
+
const agg = await ctx.messageRouter?.getThread(tid);
|
|
17811
|
+
if (!agg)
|
|
17812
|
+
return [];
|
|
17813
|
+
return agg.messages.map((env) => ({
|
|
17814
|
+
id: env.message.id,
|
|
17815
|
+
body: typeof env.message.body === 'string' ? env.message.body : String(env.message.body?.content ?? env.message.body?.text ?? ''),
|
|
17816
|
+
createdAt: env.message.createdAt,
|
|
17817
|
+
fromAgent: env.message.from?.agent,
|
|
17818
|
+
}));
|
|
17819
|
+
},
|
|
17820
|
+
};
|
|
17821
|
+
};
|
|
17742
17822
|
router.get('/messages/thread/:threadId', async (req, res) => {
|
|
17743
|
-
if (!ctx.messageRouter) {
|
|
17744
|
-
res.status(503).json({ error: 'Messaging not available' });
|
|
17745
|
-
return;
|
|
17746
|
-
}
|
|
17747
17823
|
try {
|
|
17748
17824
|
const { threadId } = req.params;
|
|
17749
|
-
|
|
17825
|
+
// Tight anchored allowlist for the REAL minted shapes (FD-7) — not UUID-only,
|
|
17826
|
+
// so a `msg-…`/`thread-…` id no longer 400s (a second F3 surface).
|
|
17827
|
+
if (!THREAD_ID_RE.test(threadId)) {
|
|
17750
17828
|
res.status(400).json({ error: 'Invalid thread ID format' });
|
|
17751
17829
|
return;
|
|
17752
17830
|
}
|
|
17831
|
+
// Robustness Phase 2 (D-C): read the CANONICAL log as the authoritative source
|
|
17832
|
+
// (UNION with a one-time bounded backfill), replacing the lossy derived
|
|
17833
|
+
// aggregate. History can only gain, never regress.
|
|
17834
|
+
const canonDeps = buildCanonicalReadDeps();
|
|
17835
|
+
if (canonDeps) {
|
|
17836
|
+
const entries = await readThreadHistoryUnion(canonDeps, threadId);
|
|
17837
|
+
const messages = entries.map((e) => ({
|
|
17838
|
+
message: {
|
|
17839
|
+
id: e.messageId,
|
|
17840
|
+
from: { agent: e.direction === 'outbound' ? ctx.config.projectName : (e.peerFingerprint ?? e.author?.agentFingerprint ?? 'peer') },
|
|
17841
|
+
body: e.textRef.kind === 'inline' ? e.textRef.text : '',
|
|
17842
|
+
createdAt: e.createdAt,
|
|
17843
|
+
threadId,
|
|
17844
|
+
},
|
|
17845
|
+
}));
|
|
17846
|
+
const last = entries.length ? entries[entries.length - 1] : null;
|
|
17847
|
+
res.json({
|
|
17848
|
+
thread: {
|
|
17849
|
+
id: threadId,
|
|
17850
|
+
messageIds: entries.map((e) => e.messageId),
|
|
17851
|
+
messageCount: entries.length,
|
|
17852
|
+
lastMessageAt: last?.createdAt ?? new Date(0).toISOString(),
|
|
17853
|
+
status: 'active',
|
|
17854
|
+
source: 'canonical-log',
|
|
17855
|
+
},
|
|
17856
|
+
messages,
|
|
17857
|
+
});
|
|
17858
|
+
return;
|
|
17859
|
+
}
|
|
17860
|
+
// Fallback (threadline disabled): the legacy derived aggregate.
|
|
17861
|
+
if (!ctx.messageRouter) {
|
|
17862
|
+
res.status(503).json({ error: 'Messaging not available' });
|
|
17863
|
+
return;
|
|
17864
|
+
}
|
|
17753
17865
|
const result = await ctx.messageRouter.getThread(threadId);
|
|
17754
17866
|
if (!result) {
|
|
17755
17867
|
res.status(404).json({ error: 'Thread not found' });
|
|
@@ -17761,6 +17873,116 @@ export function createRoutes(ctx) {
|
|
|
17761
17873
|
res.status(500).json({ error: err instanceof Error ? err.message : 'Thread query failed' });
|
|
17762
17874
|
}
|
|
17763
17875
|
});
|
|
17876
|
+
// ── Robustness Phase 2 (D-C/D-D): canonical-log read + symmetry health ────────
|
|
17877
|
+
// Bearer-gated (own-data-only), seq-cursor paginated, with a TIGHT anchored id
|
|
17878
|
+
// allowlist + a path-confinement check (traversal defense, FD-7). NOTE: these sit
|
|
17879
|
+
// under /threadline/* (which authMiddleware bypasses for relay-auth), so they
|
|
17880
|
+
// SELF-GATE on the owner bearer token. Bodies are returned tagged as UNTRUSTED
|
|
17881
|
+
// peer-authored data — never instructions.
|
|
17882
|
+
const ownerAuthed = (req) => {
|
|
17883
|
+
if (!ctx.config.authToken)
|
|
17884
|
+
return true; // no token configured → open (dev/local)
|
|
17885
|
+
const header = req.headers.authorization;
|
|
17886
|
+
if (!header?.startsWith('Bearer '))
|
|
17887
|
+
return false;
|
|
17888
|
+
try {
|
|
17889
|
+
const ha = createHash('sha256').update(header.slice(7)).digest();
|
|
17890
|
+
const hb = createHash('sha256').update(ctx.config.authToken).digest();
|
|
17891
|
+
return ha.length === hb.length && timingSafeEqual(ha, hb);
|
|
17892
|
+
}
|
|
17893
|
+
catch {
|
|
17894
|
+
return false;
|
|
17895
|
+
}
|
|
17896
|
+
};
|
|
17897
|
+
router.get('/threadline/threads/:id', async (req, res) => {
|
|
17898
|
+
if (!ownerAuthed(req)) {
|
|
17899
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
17900
|
+
return;
|
|
17901
|
+
}
|
|
17902
|
+
if (!ctx.threadLog) {
|
|
17903
|
+
res.status(503).json({ error: 'Canonical history not available' });
|
|
17904
|
+
return;
|
|
17905
|
+
}
|
|
17906
|
+
const threadId = req.params.id;
|
|
17907
|
+
// Anchored allowlist THEN a resolved-path confinement check (defense in depth).
|
|
17908
|
+
if (!THREAD_ID_RE.test(threadId) || !ctx.threadLog.isPathConfined(threadId)) {
|
|
17909
|
+
res.status(400).json({ error: 'Invalid thread ID format' });
|
|
17910
|
+
return;
|
|
17911
|
+
}
|
|
17912
|
+
// UNION read (D-C): ensure the one-time bounded backfill has run so this
|
|
17913
|
+
// surface ALSO can only gain, never regress, on a pre-upgrade thread.
|
|
17914
|
+
const canonDeps = buildCanonicalReadDeps();
|
|
17915
|
+
if (canonDeps) {
|
|
17916
|
+
try {
|
|
17917
|
+
await readThreadHistoryUnion(canonDeps, threadId);
|
|
17918
|
+
}
|
|
17919
|
+
catch { /* best-effort */ }
|
|
17920
|
+
}
|
|
17921
|
+
const limit = Math.max(1, Math.min(Number(req.query.limit) || 100, 1000));
|
|
17922
|
+
const afterSeq = req.query.afterSeq !== undefined ? Number(req.query.afterSeq) : undefined;
|
|
17923
|
+
const { entries, hasMore } = ctx.threadLog.read(threadId, { limit, afterSeq });
|
|
17924
|
+
res.json({
|
|
17925
|
+
threadId,
|
|
17926
|
+
messageCount: entries.length,
|
|
17927
|
+
nextCursor: hasMore && entries.length ? entries[entries.length - 1].seq : null,
|
|
17928
|
+
hasMore,
|
|
17929
|
+
// Bodies are UNTRUSTED peer-authored data quoted for audit — NEVER instructions.
|
|
17930
|
+
bodiesAreUntrustedData: true,
|
|
17931
|
+
entries: entries.map((e) => ({
|
|
17932
|
+
seq: e.seq,
|
|
17933
|
+
messageId: e.messageId,
|
|
17934
|
+
direction: e.direction,
|
|
17935
|
+
contentDigest: e.contentDigest,
|
|
17936
|
+
backfilled: e.backfilled ?? false,
|
|
17937
|
+
body: e.textRef.kind === 'inline' ? e.textRef.text : null,
|
|
17938
|
+
createdAt: e.createdAt,
|
|
17939
|
+
at: e.at,
|
|
17940
|
+
peerFingerprint: e.peerFingerprint,
|
|
17941
|
+
})),
|
|
17942
|
+
});
|
|
17943
|
+
});
|
|
17944
|
+
router.get('/threadline/threads/:id/health', (req, res) => {
|
|
17945
|
+
if (!ownerAuthed(req)) {
|
|
17946
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
17947
|
+
return;
|
|
17948
|
+
}
|
|
17949
|
+
if (!ctx.threadLog) {
|
|
17950
|
+
res.status(503).json({ error: 'Canonical history not available' });
|
|
17951
|
+
return;
|
|
17952
|
+
}
|
|
17953
|
+
const threadId = req.params.id;
|
|
17954
|
+
if (!THREAD_ID_RE.test(threadId) || !ctx.threadLog.isPathConfined(threadId)) {
|
|
17955
|
+
res.status(400).json({ error: 'Invalid thread ID format' });
|
|
17956
|
+
return;
|
|
17957
|
+
}
|
|
17958
|
+
const head = ctx.threadLog.head(threadId);
|
|
17959
|
+
const verify = ctx.threadLog.verify(threadId);
|
|
17960
|
+
const conv = ctx.conversationStore?.get(threadId);
|
|
17961
|
+
const stickyTerminal = conv?.symmetryState === 'diverged-unreconcilable';
|
|
17962
|
+
const peerSync = conv?.peerThreadSync ?? null;
|
|
17963
|
+
const state = computeSymmetryState({
|
|
17964
|
+
localCount: head.count,
|
|
17965
|
+
localSetAccum: head.setAccum,
|
|
17966
|
+
peerSync,
|
|
17967
|
+
hasBackfilled: ctx.threadLog.hasBackfilledLegs(threadId),
|
|
17968
|
+
localVerifyOk: verify.ok,
|
|
17969
|
+
sticky: stickyTerminal,
|
|
17970
|
+
peerEverReported: !!peerSync,
|
|
17971
|
+
});
|
|
17972
|
+
res.json({
|
|
17973
|
+
threadId,
|
|
17974
|
+
symmetryState: state,
|
|
17975
|
+
local: localThreadSync(ctx.threadLog, threadId),
|
|
17976
|
+
peer: peerSync,
|
|
17977
|
+
chainOk: verify.ok,
|
|
17978
|
+
...(verify.ok ? {} : { brokenAt: verify.brokenAt }),
|
|
17979
|
+
collisionCount: conv?.collisionCount ?? 0,
|
|
17980
|
+
// Operator recovery playbook for a local integrity fault (gemini G1).
|
|
17981
|
+
...(state === 'local-integrity-fault'
|
|
17982
|
+
? { recovery: `The canonical chain for ${threadId} is broken and cannot be repaired in place. Delete threadline/threads/${threadId}.log.jsonl and re-converge from the peer via the bounded backfill.` }
|
|
17983
|
+
: {}),
|
|
17984
|
+
});
|
|
17985
|
+
});
|
|
17764
17986
|
router.post('/messages/thread/:threadId/resolve', async (req, res) => {
|
|
17765
17987
|
if (!ctx.messageRouter) {
|
|
17766
17988
|
res.status(503).json({ error: 'Messaging not available' });
|
|
@@ -18046,6 +18268,16 @@ export function createRoutes(ctx) {
|
|
|
18046
18268
|
{
|
|
18047
18269
|
a2aDeliveryTracker: ctx.a2aDeliveryTracker,
|
|
18048
18270
|
threadResumeMap: ctx.threadResumeMap ?? null,
|
|
18271
|
+
},
|
|
18272
|
+
// Robustness Phase 2 (D-B/D-D): the canonical-history funnel + symmetry surfaces
|
|
18273
|
+
// for the verified E2E relay inbound path + participant-authorized backfill.
|
|
18274
|
+
{
|
|
18275
|
+
threadMessageRecorder: ctx.threadMessageRecorder,
|
|
18276
|
+
threadLog: ctx.threadLog,
|
|
18277
|
+
conversationStore: ctx.conversationStore,
|
|
18278
|
+
attention: ctx.telegram ?? null,
|
|
18279
|
+
backfillMaxDigestsPerRequest: ctx.config.threadline?.canonicalHistory?.backfillMaxDigestsPerRequest,
|
|
18280
|
+
backfillMaxRecordsPerResponse: ctx.config.threadline?.canonicalHistory?.backfillMaxRecordsPerResponse,
|
|
18049
18281
|
});
|
|
18050
18282
|
router.use(threadlineRoutes);
|
|
18051
18283
|
}
|
|
@@ -18398,7 +18630,38 @@ export function createRoutes(ctx) {
|
|
|
18398
18630
|
// Mint a stable UUID threadId when caller didn't provide one so
|
|
18399
18631
|
// first-contact messages aren't dropped on the recipient side. Both
|
|
18400
18632
|
// sender and recipient will agree on this id going forward.
|
|
18401
|
-
|
|
18633
|
+
let effectiveThreadId = threadId ?? randomUUID();
|
|
18634
|
+
// ── Robustness Phase 2 (D-E): conversation-discipline resolver ──────────
|
|
18635
|
+
// Default-join the canonical thread for a verified (peer, workstream) instead
|
|
18636
|
+
// of minting a fork (closes F5). DEV-GATED + DRY-RUN-FIRST: `enabled` rides the
|
|
18637
|
+
// developmentAgent gate (live on dev, dark on the fleet) and `dryRun` (default
|
|
18638
|
+
// true) only LOGS the would-join decision — the threadId is unchanged until an
|
|
18639
|
+
// operator flips dryRun off after telemetry proves the join/fork rate. The
|
|
18640
|
+
// resolver is recoverable routing, NEVER an authority: it never blocks a send.
|
|
18641
|
+
if (ctx.threadMessageRecorder) {
|
|
18642
|
+
const cdCfg = ctx.config.threadline?.canonicalHistory;
|
|
18643
|
+
const resolverEnabled = resolveDevAgentGate(cdCfg?.conversationDiscipline?.enabled, ctx.config);
|
|
18644
|
+
const resolverDryRun = cdCfg?.conversationDiscipline?.dryRun ?? true;
|
|
18645
|
+
const forkRequested = req.body?.fork === true || req.body?.newThread === true;
|
|
18646
|
+
// Best-effort VERIFIED principal available locally (never a name the peer
|
|
18647
|
+
// asserts): a fingerprint-shaped target or a curated nickname mapping.
|
|
18648
|
+
const resolverPrincipal = looksLikeFingerprint ? targetAgent : (nicknameResolvedFp ?? undefined);
|
|
18649
|
+
try {
|
|
18650
|
+
const decided = await ctx.threadMessageRecorder.resolveOutboundThread({
|
|
18651
|
+
explicitThreadId: threadId,
|
|
18652
|
+
mintedThreadId: effectiveThreadId,
|
|
18653
|
+
peerPrincipal: resolverPrincipal,
|
|
18654
|
+
subject: typeof purpose === 'string' ? purpose : undefined,
|
|
18655
|
+
fork: forkRequested,
|
|
18656
|
+
enabled: resolverEnabled,
|
|
18657
|
+
dryRun: resolverDryRun,
|
|
18658
|
+
workstreamKeyMode: cdCfg?.workstreamKeyMode ?? 'subject-slug',
|
|
18659
|
+
isHolder: true, // single-holder: this server is the holder for local conversations
|
|
18660
|
+
});
|
|
18661
|
+
effectiveThreadId = decided.threadId;
|
|
18662
|
+
}
|
|
18663
|
+
catch { /* @silent-fallback-ok: the resolver is recoverable routing; on any error keep the minted threadId */ }
|
|
18664
|
+
}
|
|
18402
18665
|
// ── Negotiator lease/voice gate (Robustness Phase 1, D-B) ───────
|
|
18403
18666
|
// Enforce single-voice: only the session that owns this conversation's lease
|
|
18404
18667
|
// may speak CONTENT in the agent's name (G1, closes F1). Ships dark
|
|
@@ -18582,6 +18845,23 @@ export function createRoutes(ctx) {
|
|
|
18582
18845
|
}
|
|
18583
18846
|
})();
|
|
18584
18847
|
const now = new Date().toISOString();
|
|
18848
|
+
// Robustness Phase 2 (D-B): log THIS outbound leg to the canonical
|
|
18849
|
+
// log BEFORE building the envelope, so the piggybacked `threadSync`
|
|
18850
|
+
// reflects the post-leg head (no transient off-by-one `diverged`).
|
|
18851
|
+
// `now` is the same createdAt stamped on the wire below ⇒ this end's
|
|
18852
|
+
// content digest matches the peer's byte-for-byte (the symmetric path).
|
|
18853
|
+
if (ctx.threadMessageRecorder) {
|
|
18854
|
+
recordThreadMessage(ctx.threadMessageRecorder, {
|
|
18855
|
+
threadId: effectiveThreadId,
|
|
18856
|
+
messageId: msgId,
|
|
18857
|
+
direction: 'outbound',
|
|
18858
|
+
body: message,
|
|
18859
|
+
createdAt: now,
|
|
18860
|
+
peerFingerprint: resolvePeerFingerprint(localTarget) || undefined,
|
|
18861
|
+
author: { machineId: ctx.meshSelfId ?? undefined, sessionName: typeof originSessionName === 'string' ? originSessionName : undefined },
|
|
18862
|
+
subject: typeof purpose === 'string' ? purpose : undefined,
|
|
18863
|
+
});
|
|
18864
|
+
}
|
|
18585
18865
|
const envelope = {
|
|
18586
18866
|
schemaVersion: 1,
|
|
18587
18867
|
message: {
|
|
@@ -18595,6 +18875,9 @@ export function createRoutes(ctx) {
|
|
|
18595
18875
|
threadId: effectiveThreadId,
|
|
18596
18876
|
createdAt: now,
|
|
18597
18877
|
},
|
|
18878
|
+
// Robustness Phase 2 (D-D): piggyback this end's threadSync so the
|
|
18879
|
+
// peer can cross-verify symmetry (additive; a legacy peer ignores it).
|
|
18880
|
+
...(ctx.threadLog ? { threadSync: localThreadSync(ctx.threadLog, effectiveThreadId) } : {}),
|
|
18598
18881
|
transport: {
|
|
18599
18882
|
relayChain: ['local'],
|
|
18600
18883
|
originServer: `http://localhost:${ctx.config.port ?? 4042}`,
|
|
@@ -18670,6 +18953,9 @@ export function createRoutes(ctx) {
|
|
|
18670
18953
|
console.warn(`[relay-send] Canonical outbox append failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
18671
18954
|
}
|
|
18672
18955
|
}
|
|
18956
|
+
// (The canonical-log append for this outbound leg already ran
|
|
18957
|
+
// BEFORE the envelope was built — see above — so its threadSync is
|
|
18958
|
+
// post-leg. The funnel is idempotent, so no double-append here.)
|
|
18673
18959
|
// Mirror outbound into Telegram bridge (relay-only — best effort).
|
|
18674
18960
|
if (ctx.telegramBridge) {
|
|
18675
18961
|
ctx.telegramBridge.mirrorOutbound({
|
|
@@ -18802,6 +19088,23 @@ export function createRoutes(ctx) {
|
|
|
18802
19088
|
console.warn(`[relay-send] Canonical outbox append failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
18803
19089
|
}
|
|
18804
19090
|
}
|
|
19091
|
+
// Robustness Phase 2 (D-B): append the relay-delivered outbound leg to the
|
|
19092
|
+
// canonical log through the funnel. The wire createdAt is stamped inside the
|
|
19093
|
+
// relay client, so this end's createdAt is best-effort (symmetry on the relay
|
|
19094
|
+
// path degrades to unverified, advisory-only); F3 holds unconditionally — the
|
|
19095
|
+
// sender's own message is now auditable per-thread regardless of symmetry.
|
|
19096
|
+
if (ctx.threadMessageRecorder) {
|
|
19097
|
+
recordThreadMessage(ctx.threadMessageRecorder, {
|
|
19098
|
+
threadId: effectiveRelayThreadId,
|
|
19099
|
+
messageId: relayMsgId,
|
|
19100
|
+
direction: 'outbound',
|
|
19101
|
+
body: message,
|
|
19102
|
+
createdAt: new Date().toISOString(),
|
|
19103
|
+
peerFingerprint: resolvedId ?? undefined,
|
|
19104
|
+
author: { machineId: ctx.meshSelfId ?? undefined, sessionName: typeof originSessionName === 'string' ? originSessionName : undefined },
|
|
19105
|
+
subject: typeof purpose === 'string' ? purpose : undefined,
|
|
19106
|
+
});
|
|
19107
|
+
}
|
|
18805
19108
|
// Mirror outbound into Telegram bridge (relay-only — best effort).
|
|
18806
19109
|
if (ctx.telegramBridge) {
|
|
18807
19110
|
ctx.telegramBridge.mirrorOutbound({
|