instar 0.28.82 → 0.28.84
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 +239 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/memoryBackfillEvidence.d.ts +69 -0
- package/dist/commands/memoryBackfillEvidence.d.ts.map +1 -0
- package/dist/commands/memoryBackfillEvidence.js +220 -0
- package/dist/commands/memoryBackfillEvidence.js.map +1 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +72 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/EvolutionManager.d.ts +73 -0
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +376 -0
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/SharedStateLedger.d.ts.map +1 -1
- package/dist/core/SharedStateLedger.js +4 -0
- package/dist/core/SharedStateLedger.js.map +1 -1
- package/dist/core/types.d.ts +90 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -1
- package/dist/memory/EvidenceRenderer.d.ts +64 -0
- package/dist/memory/EvidenceRenderer.d.ts.map +1 -0
- package/dist/memory/EvidenceRenderer.js +119 -0
- package/dist/memory/EvidenceRenderer.js.map +1 -0
- package/dist/memory/SemanticMemory.d.ts +61 -1
- package/dist/memory/SemanticMemory.d.ts.map +1 -1
- package/dist/memory/SemanticMemory.js +381 -11
- package/dist/memory/SemanticMemory.js.map +1 -1
- package/dist/server/AgentServer.d.ts +4 -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 +8 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +493 -3
- package/dist/server/routes.js.map +1 -1
- package/dist/tasks/DivergenceChecker.d.ts +89 -0
- package/dist/tasks/DivergenceChecker.d.ts.map +1 -0
- package/dist/tasks/DivergenceChecker.js +255 -0
- package/dist/tasks/DivergenceChecker.js.map +1 -0
- package/dist/tasks/LruCache.d.ts +33 -0
- package/dist/tasks/LruCache.d.ts.map +1 -0
- package/dist/tasks/LruCache.js +88 -0
- package/dist/tasks/LruCache.js.map +1 -0
- package/dist/tasks/RateLimiter.d.ts +61 -0
- package/dist/tasks/RateLimiter.d.ts.map +1 -0
- package/dist/tasks/RateLimiter.js +108 -0
- package/dist/tasks/RateLimiter.js.map +1 -0
- package/dist/tasks/TaskFlowDueWaker.d.ts +29 -0
- package/dist/tasks/TaskFlowDueWaker.d.ts.map +1 -0
- package/dist/tasks/TaskFlowDueWaker.js +73 -0
- package/dist/tasks/TaskFlowDueWaker.js.map +1 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts +40 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts.map +1 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.js +131 -0
- package/dist/tasks/TaskFlowMaintenanceSweeper.js.map +1 -0
- package/dist/tasks/TaskFlowRegistry.d.ts +180 -0
- package/dist/tasks/TaskFlowRegistry.d.ts.map +1 -0
- package/dist/tasks/TaskFlowRegistry.js +791 -0
- package/dist/tasks/TaskFlowRegistry.js.map +1 -0
- package/dist/tasks/ThreadlineFlowBridge.d.ts +54 -0
- package/dist/tasks/ThreadlineFlowBridge.d.ts.map +1 -0
- package/dist/tasks/ThreadlineFlowBridge.js +129 -0
- package/dist/tasks/ThreadlineFlowBridge.js.map +1 -0
- package/dist/tasks/task-flow-registry.store.sqlite.d.ts +77 -0
- package/dist/tasks/task-flow-registry.store.sqlite.d.ts.map +1 -0
- package/dist/tasks/task-flow-registry.store.sqlite.js +350 -0
- package/dist/tasks/task-flow-registry.store.sqlite.js.map +1 -0
- package/dist/tasks/task-flow-types.d.ts +229 -0
- package/dist/tasks/task-flow-types.d.ts.map +1 -0
- package/dist/tasks/task-flow-types.js +128 -0
- package/dist/tasks/task-flow-types.js.map +1 -0
- package/dist/threadline/ThreadlineNicknames.d.ts +88 -0
- package/dist/threadline/ThreadlineNicknames.d.ts.map +1 -0
- package/dist/threadline/ThreadlineNicknames.js +195 -0
- package/dist/threadline/ThreadlineNicknames.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +78 -78
- package/upgrades/0.28.83.md +45 -0
- package/upgrades/0.28.84.md +89 -0
- package/upgrades/side-effects/taskflow-phase1.md +130 -0
- package/upgrades/side-effects/taskflow-phase2.md +110 -0
- package/upgrades/side-effects/taskflow-phase3a.md +138 -0
- package/upgrades/side-effects/taskflow-phase5.md +234 -0
- package/upgrades/side-effects/threadline-nickname-resolver-authority.md +81 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase1.md +138 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase4.md +129 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase5.md +285 -0
package/dist/server/routes.js
CHANGED
|
@@ -78,6 +78,7 @@ import { ProcessIntegrity } from '../core/ProcessIntegrity.js';
|
|
|
78
78
|
import { getOutboundQueueStatus, cleanupDeliveredOutbound, buildAgentList } from '../messaging/GitSyncTransport.js';
|
|
79
79
|
import { verifyAgentToken, getAgentToken } from '../messaging/AgentTokenManager.js';
|
|
80
80
|
import { createThreadlineRoutes } from '../threadline/ThreadlineEndpoints.js';
|
|
81
|
+
import { ThreadlineNicknames } from '../threadline/ThreadlineNicknames.js';
|
|
81
82
|
import { ScopeCoherenceTracker } from '../core/ScopeCoherenceTracker.js';
|
|
82
83
|
import { isJunkPayload } from '../core/junk-payload.js';
|
|
83
84
|
import { detectJargon } from '../core/JargonDetector.js';
|
|
@@ -2020,6 +2021,114 @@ export function createRoutes(ctx) {
|
|
|
2020
2021
|
res.status(500).json({ error: err instanceof Error ? err.message : 'Context generation failed' });
|
|
2021
2022
|
}
|
|
2022
2023
|
});
|
|
2024
|
+
// ── WikiClaim Phase 4 — Inverse-traceability HTTP endpoints ───────
|
|
2025
|
+
//
|
|
2026
|
+
// Per docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md § Phase 4
|
|
2027
|
+
// (line 343) and § Inverse traceability (line 291).
|
|
2028
|
+
//
|
|
2029
|
+
// Both routes are thin pass-throughs to SemanticMemory's already-filtered
|
|
2030
|
+
// typed methods (`getEvidence` / `findCitations`). Privacy enforcement
|
|
2031
|
+
// lives ONE place — the storage layer's read-time filter (Phase 1) plus
|
|
2032
|
+
// the EvidenceRenderer helper (Phase 5). These routes do not re-implement
|
|
2033
|
+
// the filter; they invoke the methods and serialize the result.
|
|
2034
|
+
//
|
|
2035
|
+
// viewerScope derivation: the single bearer-token auth model has one
|
|
2036
|
+
// principal (the agent itself), which sees its own data at `private`
|
|
2037
|
+
// scope by default. Callers can request a NARROWED view via the
|
|
2038
|
+
// `?viewerScope=shared-project|shared-topic|private` query param —
|
|
2039
|
+
// useful for "what would a topic-peer see?" preview rendering in the
|
|
2040
|
+
// dashboard. Widening above the auth principal is a no-op (the cap is
|
|
2041
|
+
// `private`). Per spec § Storage and Privacy line 315 — the renderer is
|
|
2042
|
+
// the privacy boundary; this route is a read endpoint with no
|
|
2043
|
+
// judgment surface.
|
|
2044
|
+
const VALID_EVIDENCE_KINDS = new Set([
|
|
2045
|
+
'feedback', 'commit', 'session', 'document', 'message',
|
|
2046
|
+
'job-run', 'ledger-entry', 'pattern-entity', 'external-url',
|
|
2047
|
+
'supersedes-evidence',
|
|
2048
|
+
]);
|
|
2049
|
+
const VALID_VIEWER_SCOPES = new Set(['shared-project', 'shared-topic', 'private']);
|
|
2050
|
+
function resolveViewerScope(raw) {
|
|
2051
|
+
if (typeof raw === 'string' && VALID_VIEWER_SCOPES.has(raw)) {
|
|
2052
|
+
return raw;
|
|
2053
|
+
}
|
|
2054
|
+
// Default: agent has full visibility into its own DB.
|
|
2055
|
+
return 'private';
|
|
2056
|
+
}
|
|
2057
|
+
// GET /memory/evidence/by-entity/:id
|
|
2058
|
+
// Returns the entity's evidence array, viewer-scope filtered.
|
|
2059
|
+
router.get('/memory/evidence/by-entity/:id', (req, res) => {
|
|
2060
|
+
if (!ctx.semanticMemory) {
|
|
2061
|
+
res.status(503).json({ error: 'Semantic memory not enabled' });
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const entityId = req.params.id;
|
|
2066
|
+
if (!entityId || typeof entityId !== 'string') {
|
|
2067
|
+
res.status(400).json({ error: 'Missing entity id' });
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
const viewerScope = resolveViewerScope(req.query.viewerScope);
|
|
2071
|
+
// Existence + entity-level visibility check goes through the eager
|
|
2072
|
+
// helper, which returns null when the entity is hidden (or missing)
|
|
2073
|
+
// at the requested viewer scope. We deliberately collapse "entity
|
|
2074
|
+
// exists but entity-scope wider than viewer" and "entity does not
|
|
2075
|
+
// exist" into the same 404 — the spec's inverse-query non-leak rule
|
|
2076
|
+
// (§ Storage and Privacy line 316) extends to direct fetch: a viewer
|
|
2077
|
+
// at a narrower scope must not be able to probe entity existence by
|
|
2078
|
+
// diffing 404 vs 200-empty.
|
|
2079
|
+
const eager = ctx.semanticMemory.getEntityWithEvidence(entityId, viewerScope);
|
|
2080
|
+
if (!eager) {
|
|
2081
|
+
res.status(404).json({ error: 'Entity not found' });
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
res.json({
|
|
2085
|
+
entityId,
|
|
2086
|
+
viewerScope,
|
|
2087
|
+
evidence: eager.evidence,
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
catch (err) {
|
|
2091
|
+
res.status(500).json({
|
|
2092
|
+
error: err instanceof Error ? err.message : 'Evidence lookup failed',
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
// GET /memory/entities/by-evidence?kind=feedback&sourceId=fb_123
|
|
2097
|
+
// Returns entities citing (kind, sourceId), viewer-scope filtered.
|
|
2098
|
+
router.get('/memory/entities/by-evidence', (req, res) => {
|
|
2099
|
+
if (!ctx.semanticMemory) {
|
|
2100
|
+
res.status(503).json({ error: 'Semantic memory not enabled' });
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
try {
|
|
2104
|
+
const kindRaw = req.query.kind;
|
|
2105
|
+
const sourceIdRaw = req.query.sourceId;
|
|
2106
|
+
const kind = typeof kindRaw === 'string' ? kindRaw : '';
|
|
2107
|
+
const sourceId = typeof sourceIdRaw === 'string' ? sourceIdRaw : '';
|
|
2108
|
+
if (!kind || !sourceId) {
|
|
2109
|
+
res.status(400).json({ error: 'Missing required query params: kind, sourceId' });
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
if (!VALID_EVIDENCE_KINDS.has(kind)) {
|
|
2113
|
+
res.status(400).json({ error: `Invalid evidence kind: ${kind}` });
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const viewerScope = resolveViewerScope(req.query.viewerScope);
|
|
2117
|
+
const entities = ctx.semanticMemory.findCitations({ kind: kind, sourceId }, viewerScope);
|
|
2118
|
+
res.json({
|
|
2119
|
+
kind,
|
|
2120
|
+
sourceId,
|
|
2121
|
+
viewerScope,
|
|
2122
|
+
entities,
|
|
2123
|
+
totalResults: entities.length,
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
catch (err) {
|
|
2127
|
+
res.status(500).json({
|
|
2128
|
+
error: err instanceof Error ? err.message : 'Citation lookup failed',
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2023
2132
|
// ── MEMORY.md Export (Phase 6) ─────────────────────────────────
|
|
2024
2133
|
router.post('/semantic/export-memory', async (req, res) => {
|
|
2025
2134
|
if (!ctx.semanticMemory) {
|
|
@@ -9150,6 +9259,23 @@ export function createRoutes(ctx) {
|
|
|
9150
9259
|
}
|
|
9151
9260
|
}
|
|
9152
9261
|
}
|
|
9262
|
+
// ThreadlineFlowBridge: resume any TaskFlow flow waiting on a
|
|
9263
|
+
// cross-agent-callback whose correlationId matches this inbound. The
|
|
9264
|
+
// bridge runs after relay-accept (auth has passed), inspects only the
|
|
9265
|
+
// envelope, and never alters the HTTP response — failures or misses
|
|
9266
|
+
// are logged but not surfaced.
|
|
9267
|
+
if (ctx.threadlineFlowBridge) {
|
|
9268
|
+
try {
|
|
9269
|
+
const bridgeResult = await ctx.threadlineFlowBridge.consumeInbound(envelope);
|
|
9270
|
+
if (bridgeResult.resumed) {
|
|
9271
|
+
console.log(`[relay-agent] ThreadlineFlowBridge resumed ${bridgeResult.flowIds.length} flow(s): ${bridgeResult.flowIds.join(', ')}`);
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
9274
|
+
catch (err) {
|
|
9275
|
+
if (err instanceof Error)
|
|
9276
|
+
console.error('[routes] ThreadlineFlowBridge error:', err.message);
|
|
9277
|
+
}
|
|
9278
|
+
}
|
|
9153
9279
|
// If we have a ThreadlineRouter, route the message through it for
|
|
9154
9280
|
// session resume/spawn. We AWAIT the result so callers learn the real
|
|
9155
9281
|
// outcome (spawned / resumed / injected / handled:false). The message
|
|
@@ -9750,6 +9876,96 @@ export function createRoutes(ctx) {
|
|
|
9750
9876
|
res.status(413).json({ success: false, error: 'Message too large (64KB limit)' });
|
|
9751
9877
|
return;
|
|
9752
9878
|
}
|
|
9879
|
+
// Nickname-first resolution. User-curated names in nicknames.json are the
|
|
9880
|
+
// highest-authority mapping (signal-vs-authority: relay discovery is signal,
|
|
9881
|
+
// user nickname is authority). If "Dawn" maps to fingerprint X here, we
|
|
9882
|
+
// route to X regardless of what the relay's discovery cache says about
|
|
9883
|
+
// some other agent that also calls itself "Dawn". Skip when caller used
|
|
9884
|
+
// a "name:fpPrefix" qualifier (they're explicitly disambiguating) or
|
|
9885
|
+
// when the input already looks like a fingerprint.
|
|
9886
|
+
let nicknameResolvedFp = null;
|
|
9887
|
+
const looksLikeFingerprint = /^[0-9a-f]{16,64}$/i.test(targetAgent);
|
|
9888
|
+
// Parse "name:fpPrefix" once so both branches below can reuse it.
|
|
9889
|
+
const fpPrefixParse = (() => {
|
|
9890
|
+
const ci = targetAgent.lastIndexOf(':');
|
|
9891
|
+
if (ci <= 0 || ci >= targetAgent.length - 1)
|
|
9892
|
+
return null;
|
|
9893
|
+
const suffix = targetAgent.substring(ci + 1);
|
|
9894
|
+
if (!/^[0-9a-f]{4,32}$/i.test(suffix))
|
|
9895
|
+
return null;
|
|
9896
|
+
return { name: targetAgent.substring(0, ci), fpPrefix: suffix.toLowerCase() };
|
|
9897
|
+
})();
|
|
9898
|
+
const usesFpPrefixSyntax = fpPrefixParse !== null;
|
|
9899
|
+
if (!looksLikeFingerprint) {
|
|
9900
|
+
try {
|
|
9901
|
+
const nicknameStore = new ThreadlineNicknames({ stateDir: ctx.config.stateDir });
|
|
9902
|
+
// For "name:fpPrefix" inputs, look up the bare name in the nickname
|
|
9903
|
+
// store and filter the candidates by the prefix. This lets the user
|
|
9904
|
+
// disambiguate ambiguous nickname entries with the same syntax that
|
|
9905
|
+
// works for known-agents.json — the spec promises this remedy.
|
|
9906
|
+
// Without this, "Dawn:abcd" would skip nickname lookup entirely and
|
|
9907
|
+
// the documented disambiguation path would be a dead end.
|
|
9908
|
+
const lookupName = fpPrefixParse ? fpPrefixParse.name : targetAgent;
|
|
9909
|
+
const lookup = nicknameStore.resolveByName(lookupName);
|
|
9910
|
+
if (lookup && 'ambiguous' in lookup) {
|
|
9911
|
+
if (fpPrefixParse) {
|
|
9912
|
+
const matched = lookup.candidates.filter(c => c.fingerprint.toLowerCase().startsWith(fpPrefixParse.fpPrefix));
|
|
9913
|
+
if (matched.length === 1) {
|
|
9914
|
+
nicknameResolvedFp = matched[0].fingerprint;
|
|
9915
|
+
}
|
|
9916
|
+
else if (matched.length === 0) {
|
|
9917
|
+
const hints = lookup.candidates
|
|
9918
|
+
.map(c => `${c.entry.nickname}:${c.fingerprint.substring(0, 8)}`)
|
|
9919
|
+
.join(', ');
|
|
9920
|
+
res.status(409).json({
|
|
9921
|
+
success: false,
|
|
9922
|
+
error: `Nickname "${fpPrefixParse.name}" has no candidate matching prefix "${fpPrefixParse.fpPrefix}". Candidates: ${hints}.`,
|
|
9923
|
+
});
|
|
9924
|
+
return;
|
|
9925
|
+
}
|
|
9926
|
+
else {
|
|
9927
|
+
const hints = matched
|
|
9928
|
+
.map(c => `${c.entry.nickname}:${c.fingerprint.substring(0, 8)}`)
|
|
9929
|
+
.join(', ');
|
|
9930
|
+
res.status(409).json({
|
|
9931
|
+
success: false,
|
|
9932
|
+
error: `Prefix "${fpPrefixParse.fpPrefix}" still ambiguous among nickname "${fpPrefixParse.name}" candidates: ${hints}. Use a longer prefix.`,
|
|
9933
|
+
});
|
|
9934
|
+
return;
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
else {
|
|
9938
|
+
const hints = lookup.candidates
|
|
9939
|
+
.map(c => `${c.entry.nickname}:${c.fingerprint.substring(0, 8)} (${c.fingerprint})`)
|
|
9940
|
+
.join(', ');
|
|
9941
|
+
res.status(409).json({
|
|
9942
|
+
success: false,
|
|
9943
|
+
error: `Ambiguous nickname "${targetAgent}" — multiple fingerprints share this name in nicknames.json: ${hints}. Send by fingerprint or use "name:fpPrefix" syntax.`,
|
|
9944
|
+
});
|
|
9945
|
+
return;
|
|
9946
|
+
}
|
|
9947
|
+
}
|
|
9948
|
+
else if (lookup) {
|
|
9949
|
+
// Single-candidate match. If caller also passed a fpPrefix, sanity-
|
|
9950
|
+
// check that it agrees with the curated mapping; on disagreement,
|
|
9951
|
+
// honor the caller's explicit prefix (they may be telling us the
|
|
9952
|
+
// nickname is stale) and skip nickname authority for this send.
|
|
9953
|
+
if (fpPrefixParse && !lookup.fingerprint.toLowerCase().startsWith(fpPrefixParse.fpPrefix)) {
|
|
9954
|
+
console.warn(`[relay-send] Nickname/prefix disagreement for "${fpPrefixParse.name}": ` +
|
|
9955
|
+
`nickname maps to ${lookup.fingerprint.substring(0, 8)}…, ` +
|
|
9956
|
+
`caller prefix is "${fpPrefixParse.fpPrefix}". Honoring caller prefix.`);
|
|
9957
|
+
}
|
|
9958
|
+
else {
|
|
9959
|
+
nicknameResolvedFp = lookup.fingerprint;
|
|
9960
|
+
}
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
catch (err) {
|
|
9964
|
+
// Nickname store read failed — fall through to existing resolution.
|
|
9965
|
+
// Don't block sends on a corrupt nicknames file.
|
|
9966
|
+
console.warn(`[relay-send] Nickname store read failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
9967
|
+
}
|
|
9968
|
+
}
|
|
9753
9969
|
const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
9754
9970
|
// Mint a stable UUID threadId when caller didn't provide one so
|
|
9755
9971
|
// first-contact messages aren't dropped on the recipient side. Both
|
|
@@ -9775,7 +9991,14 @@ export function createRoutes(ctx) {
|
|
|
9775
9991
|
targetFpPrefix = suffix.toLowerCase();
|
|
9776
9992
|
}
|
|
9777
9993
|
}
|
|
9778
|
-
|
|
9994
|
+
// If a nickname resolved upstream, prefer fingerprint match over
|
|
9995
|
+
// name match — the user-curated mapping is authoritative.
|
|
9996
|
+
const nameMatches = nicknameResolvedFp
|
|
9997
|
+
? agents.filter(a => {
|
|
9998
|
+
const fp = (a.fingerprint || a.publicKey?.substring(0, 32) || '').toLowerCase();
|
|
9999
|
+
return fp === nicknameResolvedFp.toLowerCase();
|
|
10000
|
+
})
|
|
10001
|
+
: agents.filter(a => a.name === targetName || a.name?.toLowerCase() === targetName?.toLowerCase());
|
|
9779
10002
|
// PR-3: Fingerprint-based disambiguation. If multiple known agents
|
|
9780
10003
|
// share a name, require a `name:fpPrefix` qualifier to pick one.
|
|
9781
10004
|
// Previously this silently fell through to the relay, which then
|
|
@@ -9972,8 +10195,31 @@ export function createRoutes(ctx) {
|
|
|
9972
10195
|
return;
|
|
9973
10196
|
}
|
|
9974
10197
|
try {
|
|
9975
|
-
// Resolve name → fingerprint
|
|
9976
|
-
|
|
10198
|
+
// Resolve name → fingerprint. Nickname store wins over relay discovery
|
|
10199
|
+
// when the user has explicitly curated a mapping (set above). Falling
|
|
10200
|
+
// back to relay discovery only when no nickname matched. If the relay
|
|
10201
|
+
// happens to also know this name but maps it to a different fingerprint,
|
|
10202
|
+
// we silently override with the nickname mapping — the user's choice is
|
|
10203
|
+
// authority. Logged so the conflict is visible in operator logs.
|
|
10204
|
+
let resolvedId;
|
|
10205
|
+
if (nicknameResolvedFp) {
|
|
10206
|
+
resolvedId = nicknameResolvedFp;
|
|
10207
|
+
try {
|
|
10208
|
+
const discoveryFp = await relayClient.resolveAgent(targetAgent);
|
|
10209
|
+
if (discoveryFp && discoveryFp.toLowerCase() !== nicknameResolvedFp.toLowerCase()) {
|
|
10210
|
+
console.warn(`[relay-send] Nickname/discovery mismatch for "${targetAgent}": ` +
|
|
10211
|
+
`nickname maps to ${nicknameResolvedFp.substring(0, 8)}…, ` +
|
|
10212
|
+
`relay discovery returned ${discoveryFp.substring(0, 8)}…. ` +
|
|
10213
|
+
`Honoring user-curated nickname.`);
|
|
10214
|
+
}
|
|
10215
|
+
}
|
|
10216
|
+
catch {
|
|
10217
|
+
// Discovery probe is best-effort for the conflict warning only.
|
|
10218
|
+
}
|
|
10219
|
+
}
|
|
10220
|
+
else {
|
|
10221
|
+
resolvedId = await relayClient.resolveAgent(targetAgent);
|
|
10222
|
+
}
|
|
9977
10223
|
if (!resolvedId) {
|
|
9978
10224
|
res.status(404).json({
|
|
9979
10225
|
success: false,
|
|
@@ -11614,6 +11860,250 @@ export function createRoutes(ctx) {
|
|
|
11614
11860
|
idleExpiresAt: result.result.idleExpiresAt,
|
|
11615
11861
|
});
|
|
11616
11862
|
});
|
|
11863
|
+
// ─── TaskFlow registry (OpenClaw import — Phase 1) ──────────────────
|
|
11864
|
+
// Admin Bearer-token auth via global authMiddleware. The HTTP API exists for
|
|
11865
|
+
// debugging and out-of-process tooling; production controllers run
|
|
11866
|
+
// in-process inside the server and call the registry directly.
|
|
11867
|
+
// See docs/specs/OPENCLAW-IMPORT-TASKFLOW-SPEC.md.
|
|
11868
|
+
{
|
|
11869
|
+
const taskFlowError = (res, err) => {
|
|
11870
|
+
const e = err;
|
|
11871
|
+
if (e?.name !== 'TaskFlowError') {
|
|
11872
|
+
res.status(500).json({ error: 'internal_error', message: e?.message });
|
|
11873
|
+
return;
|
|
11874
|
+
}
|
|
11875
|
+
const status = {
|
|
11876
|
+
not_found: 404,
|
|
11877
|
+
revision_conflict: 409,
|
|
11878
|
+
already_terminal: 410,
|
|
11879
|
+
invalid_transition: 422,
|
|
11880
|
+
invalid_argument: 422,
|
|
11881
|
+
unauthorized_controller: 422,
|
|
11882
|
+
already_consumed: 422,
|
|
11883
|
+
wait_collision: 422,
|
|
11884
|
+
quota_exceeded: 429,
|
|
11885
|
+
}[e.code ?? ''] ?? 500;
|
|
11886
|
+
// Phase 5: rate-limit responses surface Retry-After header for HTTP-compliant
|
|
11887
|
+
// clients in addition to the in-body retryAfterMs hint. RFC 7231 § 7.1.3
|
|
11888
|
+
// specifies seconds; we round UP so a sub-second retry maps to 1.
|
|
11889
|
+
if (status === 429 && typeof e.detail?.retryAfterMs === 'number') {
|
|
11890
|
+
const retryAfterMs = e.detail.retryAfterMs;
|
|
11891
|
+
const seconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
11892
|
+
try {
|
|
11893
|
+
res.setHeader('Retry-After', String(seconds));
|
|
11894
|
+
}
|
|
11895
|
+
catch { /* swallow */ }
|
|
11896
|
+
}
|
|
11897
|
+
res.status(status).json({ error: e.code, ...(e.detail ?? {}), message: e.message });
|
|
11898
|
+
};
|
|
11899
|
+
router.post('/flows', async (req, res) => {
|
|
11900
|
+
if (!ctx.taskFlowRegistry) {
|
|
11901
|
+
res.status(503).json({ error: 'taskflow_not_enabled' });
|
|
11902
|
+
return;
|
|
11903
|
+
}
|
|
11904
|
+
try {
|
|
11905
|
+
const out = await ctx.taskFlowRegistry.createFlow(req.body ?? {});
|
|
11906
|
+
res.status(out.created ? 201 : 200).json(out.flow);
|
|
11907
|
+
}
|
|
11908
|
+
catch (err) {
|
|
11909
|
+
if (err instanceof Error)
|
|
11910
|
+
taskFlowError(res, err);
|
|
11911
|
+
else
|
|
11912
|
+
res.status(500).json({ error: 'internal_error' });
|
|
11913
|
+
}
|
|
11914
|
+
});
|
|
11915
|
+
router.get('/flows/:flowId', (req, res) => {
|
|
11916
|
+
if (!ctx.taskFlowRegistry) {
|
|
11917
|
+
res.status(503).json({ error: 'taskflow_not_enabled' });
|
|
11918
|
+
return;
|
|
11919
|
+
}
|
|
11920
|
+
const flow = ctx.taskFlowRegistry.getFlow(req.params.flowId);
|
|
11921
|
+
if (!flow) {
|
|
11922
|
+
res.status(404).json({ error: 'not_found', flowId: req.params.flowId });
|
|
11923
|
+
return;
|
|
11924
|
+
}
|
|
11925
|
+
// The owning controller may identify itself via header to receive the
|
|
11926
|
+
// unredacted record. Otherwise we return the redacted shape (no
|
|
11927
|
+
// stateJson, waitJson reduced to {kind}).
|
|
11928
|
+
const callerControllerId = req.header('x-taskflow-controller-id');
|
|
11929
|
+
if (callerControllerId && callerControllerId === flow.controllerId) {
|
|
11930
|
+
res.json(flow);
|
|
11931
|
+
return;
|
|
11932
|
+
}
|
|
11933
|
+
res.json(ctx.taskFlowRegistry.getRedactedFlow(req.params.flowId));
|
|
11934
|
+
});
|
|
11935
|
+
router.get('/flows/waiting', (req, res) => {
|
|
11936
|
+
if (!ctx.taskFlowRegistry) {
|
|
11937
|
+
res.status(503).json({ error: 'taskflow_not_enabled' });
|
|
11938
|
+
return;
|
|
11939
|
+
}
|
|
11940
|
+
const channel = typeof req.query.channel === 'string' ? req.query.channel : undefined;
|
|
11941
|
+
const threadId = typeof req.query.threadId === 'string' ? req.query.threadId : undefined;
|
|
11942
|
+
const peer = typeof req.query.peer === 'string' ? req.query.peer : undefined;
|
|
11943
|
+
const correlationId = typeof req.query.correlationId === 'string' ? req.query.correlationId : undefined;
|
|
11944
|
+
const waitKind = typeof req.query.waitKind === 'string' ? req.query.waitKind : undefined;
|
|
11945
|
+
if (channel && threadId && peer) {
|
|
11946
|
+
res.json({
|
|
11947
|
+
matches: ctx.taskFlowRegistry.findWaitingByReply({ channel, threadId, peer }),
|
|
11948
|
+
});
|
|
11949
|
+
return;
|
|
11950
|
+
}
|
|
11951
|
+
if (correlationId && (waitKind === 'external-call' || waitKind === 'cross-agent-callback')) {
|
|
11952
|
+
res.json({
|
|
11953
|
+
matches: ctx.taskFlowRegistry.findWaitingByCorrelation({ waitKind, correlationId }),
|
|
11954
|
+
});
|
|
11955
|
+
return;
|
|
11956
|
+
}
|
|
11957
|
+
res.status(400).json({ error: 'invalid_query', message: 'specify (channel,threadId,peer) or (correlationId,waitKind)' });
|
|
11958
|
+
});
|
|
11959
|
+
const occMutationHandler = (op) => async (req, res) => {
|
|
11960
|
+
if (!ctx.taskFlowRegistry) {
|
|
11961
|
+
res.status(503).json({ error: 'taskflow_not_enabled' });
|
|
11962
|
+
return;
|
|
11963
|
+
}
|
|
11964
|
+
const body = (req.body ?? {});
|
|
11965
|
+
const flowId = req.params.flowId;
|
|
11966
|
+
const expectedRevision = Number(body.expectedRevision);
|
|
11967
|
+
if (!Number.isFinite(expectedRevision)) {
|
|
11968
|
+
res.status(422).json({ error: 'invalid_argument', field: 'expectedRevision' });
|
|
11969
|
+
return;
|
|
11970
|
+
}
|
|
11971
|
+
const principal = body.principal;
|
|
11972
|
+
if (!principal || typeof principal !== 'object' || !('scope' in principal)) {
|
|
11973
|
+
res.status(422).json({ error: 'invalid_argument', field: 'principal' });
|
|
11974
|
+
return;
|
|
11975
|
+
}
|
|
11976
|
+
try {
|
|
11977
|
+
let result;
|
|
11978
|
+
switch (op) {
|
|
11979
|
+
case 'startStep':
|
|
11980
|
+
result = await ctx.taskFlowRegistry.startStep({
|
|
11981
|
+
flowId,
|
|
11982
|
+
expectedRevision,
|
|
11983
|
+
principal: principal,
|
|
11984
|
+
currentStep: String(body.currentStep ?? ''),
|
|
11985
|
+
});
|
|
11986
|
+
break;
|
|
11987
|
+
case 'setFlowWaiting':
|
|
11988
|
+
result = await ctx.taskFlowRegistry.setFlowWaiting({
|
|
11989
|
+
flowId,
|
|
11990
|
+
expectedRevision,
|
|
11991
|
+
principal: principal,
|
|
11992
|
+
waitJson: body.waitJson,
|
|
11993
|
+
currentStep: body.currentStep,
|
|
11994
|
+
statePatch: body.statePatch,
|
|
11995
|
+
});
|
|
11996
|
+
break;
|
|
11997
|
+
case 'resumeFlow':
|
|
11998
|
+
result = await ctx.taskFlowRegistry.resumeFlow({
|
|
11999
|
+
flowId,
|
|
12000
|
+
expectedRevision,
|
|
12001
|
+
principal: principal,
|
|
12002
|
+
waitInstanceId: String(body.waitInstanceId ?? ''),
|
|
12003
|
+
currentStep: body.currentStep,
|
|
12004
|
+
statePatch: body.statePatch,
|
|
12005
|
+
});
|
|
12006
|
+
break;
|
|
12007
|
+
case 'finishFlow':
|
|
12008
|
+
result = await ctx.taskFlowRegistry.finishFlow({
|
|
12009
|
+
flowId,
|
|
12010
|
+
expectedRevision,
|
|
12011
|
+
principal: principal,
|
|
12012
|
+
result: body.result,
|
|
12013
|
+
});
|
|
12014
|
+
break;
|
|
12015
|
+
case 'failFlow':
|
|
12016
|
+
result = await ctx.taskFlowRegistry.failFlow({
|
|
12017
|
+
flowId,
|
|
12018
|
+
expectedRevision,
|
|
12019
|
+
principal: principal,
|
|
12020
|
+
failureReason: String(body.failureReason ?? ''),
|
|
12021
|
+
});
|
|
12022
|
+
break;
|
|
12023
|
+
case 'cancelFlow':
|
|
12024
|
+
result = await ctx.taskFlowRegistry.cancelFlow({
|
|
12025
|
+
flowId,
|
|
12026
|
+
expectedRevision,
|
|
12027
|
+
principal: principal,
|
|
12028
|
+
});
|
|
12029
|
+
break;
|
|
12030
|
+
case 'markLost':
|
|
12031
|
+
result = await ctx.taskFlowRegistry.markLost({
|
|
12032
|
+
flowId,
|
|
12033
|
+
expectedRevision,
|
|
12034
|
+
ledgerEntryId: String(body.ledgerEntryId ?? ''),
|
|
12035
|
+
reason: body.reason === 'stranded' ? 'stranded' : 'lost',
|
|
12036
|
+
});
|
|
12037
|
+
break;
|
|
12038
|
+
}
|
|
12039
|
+
res.json(result);
|
|
12040
|
+
}
|
|
12041
|
+
catch (err) {
|
|
12042
|
+
if (err instanceof Error)
|
|
12043
|
+
taskFlowError(res, err);
|
|
12044
|
+
else
|
|
12045
|
+
res.status(500).json({ error: 'internal_error' });
|
|
12046
|
+
}
|
|
12047
|
+
};
|
|
12048
|
+
router.post('/flows/:flowId/start-step', occMutationHandler('startStep'));
|
|
12049
|
+
router.post('/flows/:flowId/wait', occMutationHandler('setFlowWaiting'));
|
|
12050
|
+
router.post('/flows/:flowId/resume', occMutationHandler('resumeFlow'));
|
|
12051
|
+
router.post('/flows/:flowId/finish', occMutationHandler('finishFlow'));
|
|
12052
|
+
router.post('/flows/:flowId/fail', occMutationHandler('failFlow'));
|
|
12053
|
+
router.post('/flows/:flowId/cancel-flow', occMutationHandler('cancelFlow'));
|
|
12054
|
+
router.post('/flows/:flowId/mark-lost', occMutationHandler('markLost'));
|
|
12055
|
+
router.post('/flows/:flowId/cancel-request', async (req, res) => {
|
|
12056
|
+
if (!ctx.taskFlowRegistry) {
|
|
12057
|
+
res.status(503).json({ error: 'taskflow_not_enabled' });
|
|
12058
|
+
return;
|
|
12059
|
+
}
|
|
12060
|
+
const body = (req.body ?? {});
|
|
12061
|
+
const expectedRevision = Number(body.expectedRevision);
|
|
12062
|
+
if (!Number.isFinite(expectedRevision)) {
|
|
12063
|
+
res.status(422).json({ error: 'invalid_argument', field: 'expectedRevision' });
|
|
12064
|
+
return;
|
|
12065
|
+
}
|
|
12066
|
+
try {
|
|
12067
|
+
const result = await ctx.taskFlowRegistry.requestFlowCancel({
|
|
12068
|
+
flowId: req.params.flowId,
|
|
12069
|
+
expectedRevision,
|
|
12070
|
+
requesterOrigin: body.requesterOrigin,
|
|
12071
|
+
});
|
|
12072
|
+
res.json(result);
|
|
12073
|
+
}
|
|
12074
|
+
catch (err) {
|
|
12075
|
+
if (err instanceof Error)
|
|
12076
|
+
taskFlowError(res, err);
|
|
12077
|
+
else
|
|
12078
|
+
res.status(500).json({ error: 'internal_error' });
|
|
12079
|
+
}
|
|
12080
|
+
});
|
|
12081
|
+
router.post('/flows/:flowId/ping', async (req, res) => {
|
|
12082
|
+
if (!ctx.taskFlowRegistry) {
|
|
12083
|
+
res.status(503).json({ error: 'taskflow_not_enabled' });
|
|
12084
|
+
return;
|
|
12085
|
+
}
|
|
12086
|
+
const body = (req.body ?? {});
|
|
12087
|
+
const principal = body.principal;
|
|
12088
|
+
if (!principal || principal.scope !== 'controller') {
|
|
12089
|
+
res.status(422).json({ error: 'invalid_argument', field: 'principal' });
|
|
12090
|
+
return;
|
|
12091
|
+
}
|
|
12092
|
+
try {
|
|
12093
|
+
const flow = await ctx.taskFlowRegistry.pingFlow({
|
|
12094
|
+
flowId: req.params.flowId,
|
|
12095
|
+
principal,
|
|
12096
|
+
});
|
|
12097
|
+
res.json(flow);
|
|
12098
|
+
}
|
|
12099
|
+
catch (err) {
|
|
12100
|
+
if (err instanceof Error)
|
|
12101
|
+
taskFlowError(res, err);
|
|
12102
|
+
else
|
|
12103
|
+
res.status(500).json({ error: 'internal_error' });
|
|
12104
|
+
}
|
|
12105
|
+
});
|
|
12106
|
+
}
|
|
11617
12107
|
return router;
|
|
11618
12108
|
}
|
|
11619
12109
|
export function formatUptime(ms) {
|