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.
Files changed (89) hide show
  1. package/dashboard/index.html +239 -0
  2. package/dist/cli.js +10 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/memoryBackfillEvidence.d.ts +69 -0
  5. package/dist/commands/memoryBackfillEvidence.d.ts.map +1 -0
  6. package/dist/commands/memoryBackfillEvidence.js +220 -0
  7. package/dist/commands/memoryBackfillEvidence.js.map +1 -0
  8. package/dist/commands/server.d.ts.map +1 -1
  9. package/dist/commands/server.js +72 -1
  10. package/dist/commands/server.js.map +1 -1
  11. package/dist/core/EvolutionManager.d.ts +73 -0
  12. package/dist/core/EvolutionManager.d.ts.map +1 -1
  13. package/dist/core/EvolutionManager.js +376 -0
  14. package/dist/core/EvolutionManager.js.map +1 -1
  15. package/dist/core/SharedStateLedger.d.ts.map +1 -1
  16. package/dist/core/SharedStateLedger.js +4 -0
  17. package/dist/core/SharedStateLedger.js.map +1 -1
  18. package/dist/core/types.d.ts +90 -1
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/core/types.js +8 -0
  21. package/dist/core/types.js.map +1 -1
  22. package/dist/memory/EvidenceRenderer.d.ts +64 -0
  23. package/dist/memory/EvidenceRenderer.d.ts.map +1 -0
  24. package/dist/memory/EvidenceRenderer.js +119 -0
  25. package/dist/memory/EvidenceRenderer.js.map +1 -0
  26. package/dist/memory/SemanticMemory.d.ts +61 -1
  27. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  28. package/dist/memory/SemanticMemory.js +381 -11
  29. package/dist/memory/SemanticMemory.js.map +1 -1
  30. package/dist/server/AgentServer.d.ts +4 -0
  31. package/dist/server/AgentServer.d.ts.map +1 -1
  32. package/dist/server/AgentServer.js +2 -0
  33. package/dist/server/AgentServer.js.map +1 -1
  34. package/dist/server/routes.d.ts +8 -0
  35. package/dist/server/routes.d.ts.map +1 -1
  36. package/dist/server/routes.js +493 -3
  37. package/dist/server/routes.js.map +1 -1
  38. package/dist/tasks/DivergenceChecker.d.ts +89 -0
  39. package/dist/tasks/DivergenceChecker.d.ts.map +1 -0
  40. package/dist/tasks/DivergenceChecker.js +255 -0
  41. package/dist/tasks/DivergenceChecker.js.map +1 -0
  42. package/dist/tasks/LruCache.d.ts +33 -0
  43. package/dist/tasks/LruCache.d.ts.map +1 -0
  44. package/dist/tasks/LruCache.js +88 -0
  45. package/dist/tasks/LruCache.js.map +1 -0
  46. package/dist/tasks/RateLimiter.d.ts +61 -0
  47. package/dist/tasks/RateLimiter.d.ts.map +1 -0
  48. package/dist/tasks/RateLimiter.js +108 -0
  49. package/dist/tasks/RateLimiter.js.map +1 -0
  50. package/dist/tasks/TaskFlowDueWaker.d.ts +29 -0
  51. package/dist/tasks/TaskFlowDueWaker.d.ts.map +1 -0
  52. package/dist/tasks/TaskFlowDueWaker.js +73 -0
  53. package/dist/tasks/TaskFlowDueWaker.js.map +1 -0
  54. package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts +40 -0
  55. package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts.map +1 -0
  56. package/dist/tasks/TaskFlowMaintenanceSweeper.js +131 -0
  57. package/dist/tasks/TaskFlowMaintenanceSweeper.js.map +1 -0
  58. package/dist/tasks/TaskFlowRegistry.d.ts +180 -0
  59. package/dist/tasks/TaskFlowRegistry.d.ts.map +1 -0
  60. package/dist/tasks/TaskFlowRegistry.js +791 -0
  61. package/dist/tasks/TaskFlowRegistry.js.map +1 -0
  62. package/dist/tasks/ThreadlineFlowBridge.d.ts +54 -0
  63. package/dist/tasks/ThreadlineFlowBridge.d.ts.map +1 -0
  64. package/dist/tasks/ThreadlineFlowBridge.js +129 -0
  65. package/dist/tasks/ThreadlineFlowBridge.js.map +1 -0
  66. package/dist/tasks/task-flow-registry.store.sqlite.d.ts +77 -0
  67. package/dist/tasks/task-flow-registry.store.sqlite.d.ts.map +1 -0
  68. package/dist/tasks/task-flow-registry.store.sqlite.js +350 -0
  69. package/dist/tasks/task-flow-registry.store.sqlite.js.map +1 -0
  70. package/dist/tasks/task-flow-types.d.ts +229 -0
  71. package/dist/tasks/task-flow-types.d.ts.map +1 -0
  72. package/dist/tasks/task-flow-types.js +128 -0
  73. package/dist/tasks/task-flow-types.js.map +1 -0
  74. package/dist/threadline/ThreadlineNicknames.d.ts +88 -0
  75. package/dist/threadline/ThreadlineNicknames.d.ts.map +1 -0
  76. package/dist/threadline/ThreadlineNicknames.js +195 -0
  77. package/dist/threadline/ThreadlineNicknames.js.map +1 -0
  78. package/package.json +1 -1
  79. package/src/data/builtin-manifest.json +78 -78
  80. package/upgrades/0.28.83.md +45 -0
  81. package/upgrades/0.28.84.md +89 -0
  82. package/upgrades/side-effects/taskflow-phase1.md +130 -0
  83. package/upgrades/side-effects/taskflow-phase2.md +110 -0
  84. package/upgrades/side-effects/taskflow-phase3a.md +138 -0
  85. package/upgrades/side-effects/taskflow-phase5.md +234 -0
  86. package/upgrades/side-effects/threadline-nickname-resolver-authority.md +81 -0
  87. package/upgrades/side-effects/wikiclaim-evidence-phase1.md +138 -0
  88. package/upgrades/side-effects/wikiclaim-evidence-phase4.md +129 -0
  89. package/upgrades/side-effects/wikiclaim-evidence-phase5.md +285 -0
@@ -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
- const nameMatches = agents.filter(a => a.name === targetName || a.name?.toLowerCase() === targetName?.toLowerCase());
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 if targetAgent isn't already a fingerprint
9976
- const resolvedId = await relayClient.resolveAgent(targetAgent);
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) {