instar 0.28.83 → 0.28.85

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 (114) 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/intent.d.ts.map +1 -1
  5. package/dist/commands/intent.js +29 -2
  6. package/dist/commands/intent.js.map +1 -1
  7. package/dist/commands/memoryBackfillEvidence.d.ts +69 -0
  8. package/dist/commands/memoryBackfillEvidence.d.ts.map +1 -0
  9. package/dist/commands/memoryBackfillEvidence.js +220 -0
  10. package/dist/commands/memoryBackfillEvidence.js.map +1 -0
  11. package/dist/commands/server.d.ts.map +1 -1
  12. package/dist/commands/server.js +86 -1
  13. package/dist/commands/server.js.map +1 -1
  14. package/dist/core/DecisionJournal.d.ts +66 -3
  15. package/dist/core/DecisionJournal.d.ts.map +1 -1
  16. package/dist/core/DecisionJournal.js +116 -3
  17. package/dist/core/DecisionJournal.js.map +1 -1
  18. package/dist/core/DispatchExecutor.d.ts +74 -1
  19. package/dist/core/DispatchExecutor.d.ts.map +1 -1
  20. package/dist/core/DispatchExecutor.js +120 -1
  21. package/dist/core/DispatchExecutor.js.map +1 -1
  22. package/dist/core/EvolutionManager.d.ts +133 -0
  23. package/dist/core/EvolutionManager.d.ts.map +1 -1
  24. package/dist/core/EvolutionManager.js +494 -0
  25. package/dist/core/EvolutionManager.js.map +1 -1
  26. package/dist/core/InitiativeTracker.d.ts +82 -15
  27. package/dist/core/InitiativeTracker.d.ts.map +1 -1
  28. package/dist/core/InitiativeTracker.js +502 -31
  29. package/dist/core/InitiativeTracker.js.map +1 -1
  30. package/dist/core/LearnSkillBridge.d.ts +119 -0
  31. package/dist/core/LearnSkillBridge.d.ts.map +1 -0
  32. package/dist/core/LearnSkillBridge.js +160 -0
  33. package/dist/core/LearnSkillBridge.js.map +1 -0
  34. package/dist/core/SharedStateLedger.d.ts.map +1 -1
  35. package/dist/core/SharedStateLedger.js +4 -0
  36. package/dist/core/SharedStateLedger.js.map +1 -1
  37. package/dist/core/types.d.ts +124 -1
  38. package/dist/core/types.d.ts.map +1 -1
  39. package/dist/core/types.js +8 -0
  40. package/dist/core/types.js.map +1 -1
  41. package/dist/memory/EvidenceRenderer.d.ts +64 -0
  42. package/dist/memory/EvidenceRenderer.d.ts.map +1 -0
  43. package/dist/memory/EvidenceRenderer.js +119 -0
  44. package/dist/memory/EvidenceRenderer.js.map +1 -0
  45. package/dist/memory/SemanticMemory.d.ts +61 -1
  46. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  47. package/dist/memory/SemanticMemory.js +381 -11
  48. package/dist/memory/SemanticMemory.js.map +1 -1
  49. package/dist/server/AgentServer.d.ts +4 -0
  50. package/dist/server/AgentServer.d.ts.map +1 -1
  51. package/dist/server/AgentServer.js +2 -0
  52. package/dist/server/AgentServer.js.map +1 -1
  53. package/dist/server/routes.d.ts +8 -0
  54. package/dist/server/routes.d.ts.map +1 -1
  55. package/dist/server/routes.js +500 -14
  56. package/dist/server/routes.js.map +1 -1
  57. package/dist/tasks/DivergenceChecker.d.ts +89 -0
  58. package/dist/tasks/DivergenceChecker.d.ts.map +1 -0
  59. package/dist/tasks/DivergenceChecker.js +255 -0
  60. package/dist/tasks/DivergenceChecker.js.map +1 -0
  61. package/dist/tasks/LruCache.d.ts +33 -0
  62. package/dist/tasks/LruCache.d.ts.map +1 -0
  63. package/dist/tasks/LruCache.js +88 -0
  64. package/dist/tasks/LruCache.js.map +1 -0
  65. package/dist/tasks/RateLimiter.d.ts +61 -0
  66. package/dist/tasks/RateLimiter.d.ts.map +1 -0
  67. package/dist/tasks/RateLimiter.js +108 -0
  68. package/dist/tasks/RateLimiter.js.map +1 -0
  69. package/dist/tasks/TaskFlowDueWaker.d.ts +29 -0
  70. package/dist/tasks/TaskFlowDueWaker.d.ts.map +1 -0
  71. package/dist/tasks/TaskFlowDueWaker.js +73 -0
  72. package/dist/tasks/TaskFlowDueWaker.js.map +1 -0
  73. package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts +40 -0
  74. package/dist/tasks/TaskFlowMaintenanceSweeper.d.ts.map +1 -0
  75. package/dist/tasks/TaskFlowMaintenanceSweeper.js +131 -0
  76. package/dist/tasks/TaskFlowMaintenanceSweeper.js.map +1 -0
  77. package/dist/tasks/TaskFlowRegistry.d.ts +180 -0
  78. package/dist/tasks/TaskFlowRegistry.d.ts.map +1 -0
  79. package/dist/tasks/TaskFlowRegistry.js +791 -0
  80. package/dist/tasks/TaskFlowRegistry.js.map +1 -0
  81. package/dist/tasks/ThreadlineFlowBridge.d.ts +54 -0
  82. package/dist/tasks/ThreadlineFlowBridge.d.ts.map +1 -0
  83. package/dist/tasks/ThreadlineFlowBridge.js +129 -0
  84. package/dist/tasks/ThreadlineFlowBridge.js.map +1 -0
  85. package/dist/tasks/task-flow-registry.store.sqlite.d.ts +77 -0
  86. package/dist/tasks/task-flow-registry.store.sqlite.d.ts.map +1 -0
  87. package/dist/tasks/task-flow-registry.store.sqlite.js +350 -0
  88. package/dist/tasks/task-flow-registry.store.sqlite.js.map +1 -0
  89. package/dist/tasks/task-flow-types.d.ts +229 -0
  90. package/dist/tasks/task-flow-types.d.ts.map +1 -0
  91. package/dist/tasks/task-flow-types.js +128 -0
  92. package/dist/tasks/task-flow-types.js.map +1 -0
  93. package/dist/threadline/ThreadlineMCPServer.d.ts +32 -0
  94. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  95. package/dist/threadline/ThreadlineMCPServer.js +82 -5
  96. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  97. package/dist/threadline/mcp-stdio-entry.js +56 -0
  98. package/dist/threadline/mcp-stdio-entry.js.map +1 -1
  99. package/package.json +1 -1
  100. package/src/data/builtin-manifest.json +78 -78
  101. package/upgrades/0.28.84.md +89 -0
  102. package/upgrades/0.28.85.md +64 -0
  103. package/upgrades/side-effects/0.28.83.md +27 -0
  104. package/upgrades/side-effects/taskflow-phase1.md +130 -0
  105. package/upgrades/side-effects/taskflow-phase2.md +110 -0
  106. package/upgrades/side-effects/taskflow-phase3a.md +138 -0
  107. package/upgrades/side-effects/taskflow-phase4.md +377 -0
  108. package/upgrades/side-effects/taskflow-phase5.md +234 -0
  109. package/upgrades/side-effects/threadline-discover-relay-and-trust.md +122 -0
  110. package/upgrades/side-effects/wikiclaim-evidence-phase1.md +138 -0
  111. package/upgrades/side-effects/wikiclaim-evidence-phase2.md +148 -0
  112. package/upgrades/side-effects/wikiclaim-evidence-phase3.md +125 -0
  113. package/upgrades/side-effects/wikiclaim-evidence-phase4.md +129 -0
  114. package/upgrades/side-effects/wikiclaim-evidence-phase5.md +285 -0
@@ -2021,6 +2021,114 @@ export function createRoutes(ctx) {
2021
2021
  res.status(500).json({ error: err instanceof Error ? err.message : 'Context generation failed' });
2022
2022
  }
2023
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
+ });
2024
2132
  // ── MEMORY.md Export (Phase 6) ─────────────────────────────────
2025
2133
  router.post('/semantic/export-memory', async (req, res) => {
2026
2134
  if (!ctx.semanticMemory) {
@@ -4728,7 +4836,7 @@ export function createRoutes(ctx) {
4728
4836
  }
4729
4837
  res.json(initiative);
4730
4838
  });
4731
- router.post('/initiatives', (req, res) => {
4839
+ router.post('/initiatives', async (req, res) => {
4732
4840
  if (!ctx.initiativeTracker) {
4733
4841
  res.status(503).json({ error: 'Initiative tracker not configured' });
4734
4842
  return;
@@ -4751,7 +4859,7 @@ export function createRoutes(ctx) {
4751
4859
  return;
4752
4860
  }
4753
4861
  try {
4754
- const created = ctx.initiativeTracker.create({
4862
+ const created = await ctx.initiativeTracker.create({
4755
4863
  id, title, description, phases, links, nextCheckAt,
4756
4864
  needsUser, needsUserReason, blockers,
4757
4865
  });
@@ -4761,7 +4869,7 @@ export function createRoutes(ctx) {
4761
4869
  res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
4762
4870
  }
4763
4871
  });
4764
- router.patch('/initiatives/:id', (req, res) => {
4872
+ router.patch('/initiatives/:id', async (req, res) => {
4765
4873
  if (!ctx.initiativeTracker) {
4766
4874
  res.status(503).json({ error: 'Initiative tracker not configured' });
4767
4875
  return;
@@ -4771,7 +4879,7 @@ export function createRoutes(ctx) {
4771
4879
  return;
4772
4880
  }
4773
4881
  try {
4774
- const updated = ctx.initiativeTracker.update(req.params.id, req.body ?? {});
4882
+ const updated = await ctx.initiativeTracker.update(req.params.id, req.body ?? {});
4775
4883
  res.json(updated);
4776
4884
  }
4777
4885
  catch (err) {
@@ -4780,7 +4888,7 @@ export function createRoutes(ctx) {
4780
4888
  res.status(status).json({ error: msg });
4781
4889
  }
4782
4890
  });
4783
- router.post('/initiatives/:id/phase/:phaseId', (req, res) => {
4891
+ router.post('/initiatives/:id/phase/:phaseId', async (req, res) => {
4784
4892
  if (!ctx.initiativeTracker) {
4785
4893
  res.status(503).json({ error: 'Initiative tracker not configured' });
4786
4894
  return;
@@ -4791,7 +4899,7 @@ export function createRoutes(ctx) {
4791
4899
  return;
4792
4900
  }
4793
4901
  try {
4794
- const updated = ctx.initiativeTracker.setPhaseStatus(req.params.id, req.params.phaseId, status);
4902
+ const updated = await ctx.initiativeTracker.setPhaseStatus(req.params.id, req.params.phaseId, status);
4795
4903
  res.json(updated);
4796
4904
  }
4797
4905
  catch (err) {
@@ -4799,12 +4907,12 @@ export function createRoutes(ctx) {
4799
4907
  res.status(msg.includes('not found') ? 404 : 400).json({ error: msg });
4800
4908
  }
4801
4909
  });
4802
- router.delete('/initiatives/:id', (req, res) => {
4910
+ router.delete('/initiatives/:id', async (req, res) => {
4803
4911
  if (!ctx.initiativeTracker) {
4804
4912
  res.status(503).json({ error: 'Initiative tracker not configured' });
4805
4913
  return;
4806
4914
  }
4807
- const removed = ctx.initiativeTracker.remove(req.params.id);
4915
+ const removed = await ctx.initiativeTracker.remove(req.params.id);
4808
4916
  if (!removed) {
4809
4917
  res.status(404).json({ error: 'initiative not found' });
4810
4918
  return;
@@ -6723,12 +6831,12 @@ export function createRoutes(ctx) {
6723
6831
  const applied = req.query.applied !== undefined ? req.query.applied === 'true' : undefined;
6724
6832
  res.json({ learnings: ctx.evolution.listLearnings({ category, applied }) });
6725
6833
  });
6726
- router.post('/evolution/learnings', (req, res) => {
6834
+ router.post('/evolution/learnings', async (req, res) => {
6727
6835
  if (!ctx.evolution) {
6728
6836
  res.status(503).json({ error: 'Evolution system not configured' });
6729
6837
  return;
6730
6838
  }
6731
- const { title, category, description, source, tags, evolutionRelevance } = req.body;
6839
+ const { title, category, description, source, tags, evolutionRelevance, context, documentFallback, evidence: explicitEvidence } = req.body;
6732
6840
  if (!title || typeof title !== 'string' || title.length > 500) {
6733
6841
  res.status(400).json({ error: '"title" must be a string under 500 characters' });
6734
6842
  return;
@@ -6737,6 +6845,43 @@ export function createRoutes(ctx) {
6737
6845
  res.status(400).json({ error: '"description" is required' });
6738
6846
  return;
6739
6847
  }
6848
+ // WikiClaim Phase 3 (spec § Producers line 268, § Migration Plan line 341):
6849
+ // /learn must cite at least one evidence row. Either:
6850
+ // (a) caller passes `evidence: MemoryEvidence[]` directly (advanced),
6851
+ // (b) caller passes `context: string` and we auto-derive sessions/messages,
6852
+ // (c) caller passes `documentFallback: {sourceId, path}` for prompted-source.
6853
+ // Combining (b)+(c) is allowed.
6854
+ let derivedEvidence = [];
6855
+ let externalReferences = [];
6856
+ let pendingDocumentRef;
6857
+ try {
6858
+ if (Array.isArray(explicitEvidence) && explicitEvidence.length > 0) {
6859
+ derivedEvidence = explicitEvidence;
6860
+ }
6861
+ else {
6862
+ const { buildLearnEvidence, LearnEvidenceError } = await import('../core/LearnSkillBridge.js');
6863
+ try {
6864
+ const built = buildLearnEvidence({
6865
+ context: typeof context === 'string' ? context : `${title}\n\n${description}`,
6866
+ documentFallback,
6867
+ });
6868
+ derivedEvidence = built.evidence;
6869
+ externalReferences = built.externalReferences;
6870
+ pendingDocumentRef = built.pendingDocumentRef;
6871
+ }
6872
+ catch (err) {
6873
+ if (err instanceof LearnEvidenceError) {
6874
+ res.status(400).json({ error: err.message });
6875
+ return;
6876
+ }
6877
+ throw err;
6878
+ }
6879
+ }
6880
+ }
6881
+ catch (err) {
6882
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to derive evidence' });
6883
+ return;
6884
+ }
6740
6885
  const learning = ctx.evolution.addLearning({
6741
6886
  title, category: category || 'general', description,
6742
6887
  source: source || { discoveredAt: new Date().toISOString() },
@@ -6753,7 +6898,18 @@ export function createRoutes(ctx) {
6753
6898
  soulNudge = 'This learning may be identity-relevant. Consider updating soul.md or running /reflect.';
6754
6899
  }
6755
6900
  }
6756
- res.status(201).json({ ...learning, soulNudge });
6901
+ // WikiClaim Phase 3: surface derived evidence + external references on
6902
+ // the response so callers can confirm the bridge resolved their context.
6903
+ // Spec § Producers line 228 — LearnSkill kinds are message|session;
6904
+ // externalReferences carry feedback/commit refs that the caller routes
6905
+ // through the appropriate downstream producer.
6906
+ res.status(201).json({
6907
+ ...learning,
6908
+ soulNudge,
6909
+ evidence: derivedEvidence,
6910
+ externalReferences,
6911
+ ...(pendingDocumentRef ? { pendingDocumentRef } : {}),
6912
+ });
6757
6913
  });
6758
6914
  router.patch('/evolution/learnings/:id/apply', (req, res) => {
6759
6915
  if (!ctx.evolution) {
@@ -7408,14 +7564,46 @@ export function createRoutes(ctx) {
7408
7564
  router.post('/intent/journal', async (req, res) => {
7409
7565
  try {
7410
7566
  const { DecisionJournal } = await import('../core/DecisionJournal.js');
7567
+ const { EvidencePolicyError } = await import('../memory/SemanticMemory.js');
7411
7568
  const journal = new DecisionJournal(ctx.config.stateDir);
7412
- const { sessionId, decision, ...rest } = req.body || {};
7569
+ const { sessionId, decision, evidence, ...rest } = req.body || {};
7413
7570
  if (!sessionId || !decision) {
7414
7571
  res.status(400).json({ error: 'sessionId and decision are required' });
7415
7572
  return;
7416
7573
  }
7417
- const entry = journal.log({ sessionId, decision, ...rest });
7418
- res.status(201).json(entry);
7574
+ // WikiClaim Phase 3 (spec § Producers line 258): every decision must
7575
+ // cite at least one evidence row. The route accepts an explicit
7576
+ // `evidence` array, OR — when omitted — synthesizes a minimum-viable
7577
+ // `session` evidence row from the request's `sessionId` (the auth-
7578
+ // context proxy available at this HTTP layer). Synthesis keeps the
7579
+ // legacy POST shape working while still satisfying the policy gate;
7580
+ // explicit evidence always overrides synthesis. Spec § Producers
7581
+ // line 227: `session` is in the DecisionJournal allowlist. Spec
7582
+ // § Storage and Privacy line 333: synthetic sourceIds are tolerated
7583
+ // (consumers handle dangling refs).
7584
+ let effectiveEvidence = Array.isArray(evidence) ? evidence : [];
7585
+ if (effectiveEvidence.length === 0) {
7586
+ effectiveEvidence = [{
7587
+ kind: 'session',
7588
+ sourceId: `session:${sessionId}`,
7589
+ weight: 0.5,
7590
+ confidence: 0.5,
7591
+ privacyTier: 'private',
7592
+ note: 'auto-synthesized from request session (no explicit evidence)',
7593
+ updatedAt: new Date().toISOString(),
7594
+ }];
7595
+ }
7596
+ try {
7597
+ const entry = journal.log({ sessionId, decision, ...rest }, effectiveEvidence);
7598
+ res.status(201).json(entry);
7599
+ }
7600
+ catch (err) {
7601
+ if (err instanceof EvidencePolicyError) {
7602
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
7603
+ return;
7604
+ }
7605
+ throw err;
7606
+ }
7419
7607
  }
7420
7608
  catch (err) {
7421
7609
  res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to log decision' });
@@ -9151,6 +9339,23 @@ export function createRoutes(ctx) {
9151
9339
  }
9152
9340
  }
9153
9341
  }
9342
+ // ThreadlineFlowBridge: resume any TaskFlow flow waiting on a
9343
+ // cross-agent-callback whose correlationId matches this inbound. The
9344
+ // bridge runs after relay-accept (auth has passed), inspects only the
9345
+ // envelope, and never alters the HTTP response — failures or misses
9346
+ // are logged but not surfaced.
9347
+ if (ctx.threadlineFlowBridge) {
9348
+ try {
9349
+ const bridgeResult = await ctx.threadlineFlowBridge.consumeInbound(envelope);
9350
+ if (bridgeResult.resumed) {
9351
+ console.log(`[relay-agent] ThreadlineFlowBridge resumed ${bridgeResult.flowIds.length} flow(s): ${bridgeResult.flowIds.join(', ')}`);
9352
+ }
9353
+ }
9354
+ catch (err) {
9355
+ if (err instanceof Error)
9356
+ console.error('[routes] ThreadlineFlowBridge error:', err.message);
9357
+ }
9358
+ }
9154
9359
  // If we have a ThreadlineRouter, route the message through it for
9155
9360
  // session resume/spawn. We AWAIT the result so callers learn the real
9156
9361
  // outcome (spawned / resumed / injected / handled:false). The message
@@ -10162,6 +10367,43 @@ export function createRoutes(ctx) {
10162
10367
  });
10163
10368
  }
10164
10369
  });
10370
+ // ── Threadline Relay Discover ────────────────────────────────────────
10371
+ // Proxies a discover query through the agent server's relay WebSocket so
10372
+ // the MCP stdio subprocess can ask the relay for its live presence registry
10373
+ // (the MCP subprocess doesn't have its own relay client — same arrangement
10374
+ // as /threadline/relay-send above).
10375
+ router.post('/threadline/relay-discover', async (req, res) => {
10376
+ const relayClient = ctx.threadlineRelayClient;
10377
+ if (!relayClient || relayClient.connectionState !== 'connected') {
10378
+ res.status(503).json({
10379
+ success: false,
10380
+ error: 'Relay not connected',
10381
+ connectionState: relayClient?.connectionState ?? 'absent',
10382
+ });
10383
+ return;
10384
+ }
10385
+ const filter = (req.body && typeof req.body === 'object') ? req.body.filter : undefined;
10386
+ try {
10387
+ const agents = await relayClient.discover(filter);
10388
+ res.json({
10389
+ success: true,
10390
+ agents: agents.map(a => ({
10391
+ agentId: a.agentId,
10392
+ name: a.name,
10393
+ publicKey: Buffer.isBuffer(a.publicKey) ? a.publicKey.toString('hex') : a.publicKey,
10394
+ framework: a.framework,
10395
+ capabilities: a.capabilities,
10396
+ lastSeen: a.lastSeen,
10397
+ })),
10398
+ });
10399
+ }
10400
+ catch (err) {
10401
+ res.status(500).json({
10402
+ success: false,
10403
+ error: err instanceof Error ? err.message : 'Discover failed',
10404
+ });
10405
+ }
10406
+ });
10165
10407
  // ── Response Review Pipeline (Coherence Gate) ────────────────────────
10166
10408
  //
10167
10409
  // Evaluates agent responses before delivery. Implements PEL + Gate + Specialist
@@ -11735,6 +11977,250 @@ export function createRoutes(ctx) {
11735
11977
  idleExpiresAt: result.result.idleExpiresAt,
11736
11978
  });
11737
11979
  });
11980
+ // ─── TaskFlow registry (OpenClaw import — Phase 1) ──────────────────
11981
+ // Admin Bearer-token auth via global authMiddleware. The HTTP API exists for
11982
+ // debugging and out-of-process tooling; production controllers run
11983
+ // in-process inside the server and call the registry directly.
11984
+ // See docs/specs/OPENCLAW-IMPORT-TASKFLOW-SPEC.md.
11985
+ {
11986
+ const taskFlowError = (res, err) => {
11987
+ const e = err;
11988
+ if (e?.name !== 'TaskFlowError') {
11989
+ res.status(500).json({ error: 'internal_error', message: e?.message });
11990
+ return;
11991
+ }
11992
+ const status = {
11993
+ not_found: 404,
11994
+ revision_conflict: 409,
11995
+ already_terminal: 410,
11996
+ invalid_transition: 422,
11997
+ invalid_argument: 422,
11998
+ unauthorized_controller: 422,
11999
+ already_consumed: 422,
12000
+ wait_collision: 422,
12001
+ quota_exceeded: 429,
12002
+ }[e.code ?? ''] ?? 500;
12003
+ // Phase 5: rate-limit responses surface Retry-After header for HTTP-compliant
12004
+ // clients in addition to the in-body retryAfterMs hint. RFC 7231 § 7.1.3
12005
+ // specifies seconds; we round UP so a sub-second retry maps to 1.
12006
+ if (status === 429 && typeof e.detail?.retryAfterMs === 'number') {
12007
+ const retryAfterMs = e.detail.retryAfterMs;
12008
+ const seconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
12009
+ try {
12010
+ res.setHeader('Retry-After', String(seconds));
12011
+ }
12012
+ catch { /* swallow */ }
12013
+ }
12014
+ res.status(status).json({ error: e.code, ...(e.detail ?? {}), message: e.message });
12015
+ };
12016
+ router.post('/flows', async (req, res) => {
12017
+ if (!ctx.taskFlowRegistry) {
12018
+ res.status(503).json({ error: 'taskflow_not_enabled' });
12019
+ return;
12020
+ }
12021
+ try {
12022
+ const out = await ctx.taskFlowRegistry.createFlow(req.body ?? {});
12023
+ res.status(out.created ? 201 : 200).json(out.flow);
12024
+ }
12025
+ catch (err) {
12026
+ if (err instanceof Error)
12027
+ taskFlowError(res, err);
12028
+ else
12029
+ res.status(500).json({ error: 'internal_error' });
12030
+ }
12031
+ });
12032
+ router.get('/flows/:flowId', (req, res) => {
12033
+ if (!ctx.taskFlowRegistry) {
12034
+ res.status(503).json({ error: 'taskflow_not_enabled' });
12035
+ return;
12036
+ }
12037
+ const flow = ctx.taskFlowRegistry.getFlow(req.params.flowId);
12038
+ if (!flow) {
12039
+ res.status(404).json({ error: 'not_found', flowId: req.params.flowId });
12040
+ return;
12041
+ }
12042
+ // The owning controller may identify itself via header to receive the
12043
+ // unredacted record. Otherwise we return the redacted shape (no
12044
+ // stateJson, waitJson reduced to {kind}).
12045
+ const callerControllerId = req.header('x-taskflow-controller-id');
12046
+ if (callerControllerId && callerControllerId === flow.controllerId) {
12047
+ res.json(flow);
12048
+ return;
12049
+ }
12050
+ res.json(ctx.taskFlowRegistry.getRedactedFlow(req.params.flowId));
12051
+ });
12052
+ router.get('/flows/waiting', (req, res) => {
12053
+ if (!ctx.taskFlowRegistry) {
12054
+ res.status(503).json({ error: 'taskflow_not_enabled' });
12055
+ return;
12056
+ }
12057
+ const channel = typeof req.query.channel === 'string' ? req.query.channel : undefined;
12058
+ const threadId = typeof req.query.threadId === 'string' ? req.query.threadId : undefined;
12059
+ const peer = typeof req.query.peer === 'string' ? req.query.peer : undefined;
12060
+ const correlationId = typeof req.query.correlationId === 'string' ? req.query.correlationId : undefined;
12061
+ const waitKind = typeof req.query.waitKind === 'string' ? req.query.waitKind : undefined;
12062
+ if (channel && threadId && peer) {
12063
+ res.json({
12064
+ matches: ctx.taskFlowRegistry.findWaitingByReply({ channel, threadId, peer }),
12065
+ });
12066
+ return;
12067
+ }
12068
+ if (correlationId && (waitKind === 'external-call' || waitKind === 'cross-agent-callback')) {
12069
+ res.json({
12070
+ matches: ctx.taskFlowRegistry.findWaitingByCorrelation({ waitKind, correlationId }),
12071
+ });
12072
+ return;
12073
+ }
12074
+ res.status(400).json({ error: 'invalid_query', message: 'specify (channel,threadId,peer) or (correlationId,waitKind)' });
12075
+ });
12076
+ const occMutationHandler = (op) => async (req, res) => {
12077
+ if (!ctx.taskFlowRegistry) {
12078
+ res.status(503).json({ error: 'taskflow_not_enabled' });
12079
+ return;
12080
+ }
12081
+ const body = (req.body ?? {});
12082
+ const flowId = req.params.flowId;
12083
+ const expectedRevision = Number(body.expectedRevision);
12084
+ if (!Number.isFinite(expectedRevision)) {
12085
+ res.status(422).json({ error: 'invalid_argument', field: 'expectedRevision' });
12086
+ return;
12087
+ }
12088
+ const principal = body.principal;
12089
+ if (!principal || typeof principal !== 'object' || !('scope' in principal)) {
12090
+ res.status(422).json({ error: 'invalid_argument', field: 'principal' });
12091
+ return;
12092
+ }
12093
+ try {
12094
+ let result;
12095
+ switch (op) {
12096
+ case 'startStep':
12097
+ result = await ctx.taskFlowRegistry.startStep({
12098
+ flowId,
12099
+ expectedRevision,
12100
+ principal: principal,
12101
+ currentStep: String(body.currentStep ?? ''),
12102
+ });
12103
+ break;
12104
+ case 'setFlowWaiting':
12105
+ result = await ctx.taskFlowRegistry.setFlowWaiting({
12106
+ flowId,
12107
+ expectedRevision,
12108
+ principal: principal,
12109
+ waitJson: body.waitJson,
12110
+ currentStep: body.currentStep,
12111
+ statePatch: body.statePatch,
12112
+ });
12113
+ break;
12114
+ case 'resumeFlow':
12115
+ result = await ctx.taskFlowRegistry.resumeFlow({
12116
+ flowId,
12117
+ expectedRevision,
12118
+ principal: principal,
12119
+ waitInstanceId: String(body.waitInstanceId ?? ''),
12120
+ currentStep: body.currentStep,
12121
+ statePatch: body.statePatch,
12122
+ });
12123
+ break;
12124
+ case 'finishFlow':
12125
+ result = await ctx.taskFlowRegistry.finishFlow({
12126
+ flowId,
12127
+ expectedRevision,
12128
+ principal: principal,
12129
+ result: body.result,
12130
+ });
12131
+ break;
12132
+ case 'failFlow':
12133
+ result = await ctx.taskFlowRegistry.failFlow({
12134
+ flowId,
12135
+ expectedRevision,
12136
+ principal: principal,
12137
+ failureReason: String(body.failureReason ?? ''),
12138
+ });
12139
+ break;
12140
+ case 'cancelFlow':
12141
+ result = await ctx.taskFlowRegistry.cancelFlow({
12142
+ flowId,
12143
+ expectedRevision,
12144
+ principal: principal,
12145
+ });
12146
+ break;
12147
+ case 'markLost':
12148
+ result = await ctx.taskFlowRegistry.markLost({
12149
+ flowId,
12150
+ expectedRevision,
12151
+ ledgerEntryId: String(body.ledgerEntryId ?? ''),
12152
+ reason: body.reason === 'stranded' ? 'stranded' : 'lost',
12153
+ });
12154
+ break;
12155
+ }
12156
+ res.json(result);
12157
+ }
12158
+ catch (err) {
12159
+ if (err instanceof Error)
12160
+ taskFlowError(res, err);
12161
+ else
12162
+ res.status(500).json({ error: 'internal_error' });
12163
+ }
12164
+ };
12165
+ router.post('/flows/:flowId/start-step', occMutationHandler('startStep'));
12166
+ router.post('/flows/:flowId/wait', occMutationHandler('setFlowWaiting'));
12167
+ router.post('/flows/:flowId/resume', occMutationHandler('resumeFlow'));
12168
+ router.post('/flows/:flowId/finish', occMutationHandler('finishFlow'));
12169
+ router.post('/flows/:flowId/fail', occMutationHandler('failFlow'));
12170
+ router.post('/flows/:flowId/cancel-flow', occMutationHandler('cancelFlow'));
12171
+ router.post('/flows/:flowId/mark-lost', occMutationHandler('markLost'));
12172
+ router.post('/flows/:flowId/cancel-request', async (req, res) => {
12173
+ if (!ctx.taskFlowRegistry) {
12174
+ res.status(503).json({ error: 'taskflow_not_enabled' });
12175
+ return;
12176
+ }
12177
+ const body = (req.body ?? {});
12178
+ const expectedRevision = Number(body.expectedRevision);
12179
+ if (!Number.isFinite(expectedRevision)) {
12180
+ res.status(422).json({ error: 'invalid_argument', field: 'expectedRevision' });
12181
+ return;
12182
+ }
12183
+ try {
12184
+ const result = await ctx.taskFlowRegistry.requestFlowCancel({
12185
+ flowId: req.params.flowId,
12186
+ expectedRevision,
12187
+ requesterOrigin: body.requesterOrigin,
12188
+ });
12189
+ res.json(result);
12190
+ }
12191
+ catch (err) {
12192
+ if (err instanceof Error)
12193
+ taskFlowError(res, err);
12194
+ else
12195
+ res.status(500).json({ error: 'internal_error' });
12196
+ }
12197
+ });
12198
+ router.post('/flows/:flowId/ping', async (req, res) => {
12199
+ if (!ctx.taskFlowRegistry) {
12200
+ res.status(503).json({ error: 'taskflow_not_enabled' });
12201
+ return;
12202
+ }
12203
+ const body = (req.body ?? {});
12204
+ const principal = body.principal;
12205
+ if (!principal || principal.scope !== 'controller') {
12206
+ res.status(422).json({ error: 'invalid_argument', field: 'principal' });
12207
+ return;
12208
+ }
12209
+ try {
12210
+ const flow = await ctx.taskFlowRegistry.pingFlow({
12211
+ flowId: req.params.flowId,
12212
+ principal,
12213
+ });
12214
+ res.json(flow);
12215
+ }
12216
+ catch (err) {
12217
+ if (err instanceof Error)
12218
+ taskFlowError(res, err);
12219
+ else
12220
+ res.status(500).json({ error: 'internal_error' });
12221
+ }
12222
+ });
12223
+ }
11738
12224
  return router;
11739
12225
  }
11740
12226
  export function formatUptime(ms) {