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.
- package/dashboard/index.html +239 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +29 -2
- package/dist/commands/intent.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 +86 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/DecisionJournal.d.ts +66 -3
- package/dist/core/DecisionJournal.d.ts.map +1 -1
- package/dist/core/DecisionJournal.js +116 -3
- package/dist/core/DecisionJournal.js.map +1 -1
- package/dist/core/DispatchExecutor.d.ts +74 -1
- package/dist/core/DispatchExecutor.d.ts.map +1 -1
- package/dist/core/DispatchExecutor.js +120 -1
- package/dist/core/DispatchExecutor.js.map +1 -1
- package/dist/core/EvolutionManager.d.ts +133 -0
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +494 -0
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/InitiativeTracker.d.ts +82 -15
- package/dist/core/InitiativeTracker.d.ts.map +1 -1
- package/dist/core/InitiativeTracker.js +502 -31
- package/dist/core/InitiativeTracker.js.map +1 -1
- package/dist/core/LearnSkillBridge.d.ts +119 -0
- package/dist/core/LearnSkillBridge.d.ts.map +1 -0
- package/dist/core/LearnSkillBridge.js +160 -0
- package/dist/core/LearnSkillBridge.js.map +1 -0
- 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 +124 -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 +500 -14
- 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/ThreadlineMCPServer.d.ts +32 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +82 -5
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/mcp-stdio-entry.js +56 -0
- package/dist/threadline/mcp-stdio-entry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +78 -78
- package/upgrades/0.28.84.md +89 -0
- package/upgrades/0.28.85.md +64 -0
- package/upgrades/side-effects/0.28.83.md +27 -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-phase4.md +377 -0
- package/upgrades/side-effects/taskflow-phase5.md +234 -0
- package/upgrades/side-effects/threadline-discover-relay-and-trust.md +122 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase1.md +138 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase2.md +148 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase3.md +125 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase4.md +129 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase5.md +285 -0
|
@@ -17,16 +17,497 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { SafeFsExecutor } from './SafeFsExecutor.js';
|
|
20
|
+
import { TaskFlowError } from '../tasks/task-flow-types.js';
|
|
21
|
+
/**
|
|
22
|
+
* TaskFlow controller identity for EvolutionManager (Phase 3a dual-write).
|
|
23
|
+
* Every proposal's flow is owned by this controllerId; the registry uses it
|
|
24
|
+
* for OCC scope checks. Single-instance per server process.
|
|
25
|
+
*/
|
|
26
|
+
const EVOLUTION_TASKFLOW_CONTROLLER_ID = 'EvolutionManager';
|
|
27
|
+
function statusToFlowAction(prev, next, resolution) {
|
|
28
|
+
// Initial create-from-proposed is handled in addProposal directly.
|
|
29
|
+
if (prev === null && next === 'proposed')
|
|
30
|
+
return { kind: 'create' };
|
|
31
|
+
if (prev === next)
|
|
32
|
+
return { kind: 'noop' };
|
|
33
|
+
switch (next) {
|
|
34
|
+
case 'approved':
|
|
35
|
+
return { kind: 'start', step: 'approved' };
|
|
36
|
+
case 'in_progress':
|
|
37
|
+
return { kind: 'start', step: 'in_progress' };
|
|
38
|
+
case 'implemented':
|
|
39
|
+
return { kind: 'finish' };
|
|
40
|
+
case 'rejected':
|
|
41
|
+
return { kind: 'fail', reason: resolution ?? 'rejected' };
|
|
42
|
+
case 'deferred':
|
|
43
|
+
return { kind: 'cancel', reason: resolution ?? 'deferred' };
|
|
44
|
+
case 'proposed':
|
|
45
|
+
// Re-opening is not a legal transition; treat as no-op for v1.
|
|
46
|
+
return { kind: 'noop' };
|
|
47
|
+
default:
|
|
48
|
+
return { kind: 'noop' };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Owner-key for a proposal-flow. Deterministic — used as both the TaskFlow
|
|
53
|
+
* `ownerKey` and the basis of the `idempotencyKey`. Phase 3b's cutover
|
|
54
|
+
* gate relies on `evolution:cluster:<id>` being a stable bidirectional join.
|
|
55
|
+
*/
|
|
56
|
+
function ownerKeyForProposal(proposalId) {
|
|
57
|
+
return `evolution:cluster:${proposalId}`;
|
|
58
|
+
}
|
|
20
59
|
export class EvolutionManager {
|
|
21
60
|
stateDir;
|
|
22
61
|
config;
|
|
23
62
|
trustElevationTracker = null;
|
|
24
63
|
autonomousEvolution = null;
|
|
25
64
|
autonomyManager = null;
|
|
65
|
+
// ── TaskFlow Phase 3a dual-write fields ──────────────────────────
|
|
66
|
+
/** Registry handle (set when TaskFlow is enabled). */
|
|
67
|
+
taskFlowRegistry = null;
|
|
68
|
+
/** Per-process controller-instance id (server uuid). */
|
|
69
|
+
taskFlowControllerInstanceId = null;
|
|
70
|
+
/**
|
|
71
|
+
* Set to true by DivergenceChecker when state-divergence is detected.
|
|
72
|
+
* When true, EvolutionManager continues JSON-only writes and DOES NOT
|
|
73
|
+
* dual-write to TaskFlow. The signal is reset on next divergence pass that
|
|
74
|
+
* reports zero divergence. This is a signal-emitted brake, not a hard gate.
|
|
75
|
+
*/
|
|
76
|
+
taskFlowShadowWritesHalted = false;
|
|
77
|
+
/** Most recent reason for halting; surfaced in /health and tests. */
|
|
78
|
+
taskFlowShadowWritesHaltReason = null;
|
|
79
|
+
/**
|
|
80
|
+
* Source of the most recent halt. DivergenceChecker uses this to ensure it
|
|
81
|
+
* only auto-clears halts that DivergenceChecker itself imposed — an operator
|
|
82
|
+
* or sibling system's manual halt stays in place until that source clears it.
|
|
83
|
+
*/
|
|
84
|
+
taskFlowShadowWritesHaltSource = null;
|
|
85
|
+
// ── WikiClaim Evidence Phase 2 fields ───────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* SemanticMemory handle. When wired, EvolutionManager creates a `pattern`
|
|
88
|
+
* MemoryEntity for each new proposal (the cluster) and populates evidence
|
|
89
|
+
* rows linking the cluster to its constituent feedback IDs / commit SHAs /
|
|
90
|
+
* session IDs. JSON state remains the source of truth for proposals; the
|
|
91
|
+
* cluster entity is a parallel surface for inverse-traceability queries.
|
|
92
|
+
*
|
|
93
|
+
* See docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md § Producers.
|
|
94
|
+
*/
|
|
95
|
+
semanticMemory = null;
|
|
26
96
|
constructor(config) {
|
|
27
97
|
this.config = config;
|
|
28
98
|
this.stateDir = config.stateDir;
|
|
29
99
|
}
|
|
100
|
+
// ── TaskFlow wiring ─────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Wire a TaskFlow registry for Phase 3a dual-write. After this call:
|
|
103
|
+
* - addProposal / updateProposalStatus shadow-write to TaskFlow
|
|
104
|
+
* - migrateExistingToTaskFlow() backfills any in-flight clusters
|
|
105
|
+
* - DivergenceChecker can call setShadowWritesHalted() to brake writes
|
|
106
|
+
*
|
|
107
|
+
* Idempotent — safe to call multiple times; subsequent calls overwrite.
|
|
108
|
+
*/
|
|
109
|
+
setTaskFlowRegistry(registry, controllerInstanceId) {
|
|
110
|
+
this.taskFlowRegistry = registry;
|
|
111
|
+
this.taskFlowControllerInstanceId = controllerInstanceId;
|
|
112
|
+
}
|
|
113
|
+
getTaskFlowRegistry() {
|
|
114
|
+
return this.taskFlowRegistry;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Halt or resume TaskFlow shadow-writes. Called by DivergenceChecker on a
|
|
118
|
+
* mismatch. Signal-vs-authority compliance: this is a signal-consumed brake,
|
|
119
|
+
* not a brittle blocker — the divergence checker decides, EvolutionManager
|
|
120
|
+
* obeys. JSON writes continue regardless (read-authoritative TaskFlow is
|
|
121
|
+
* shadow-mode in Phase 3a).
|
|
122
|
+
*
|
|
123
|
+
* The optional `source` parameter tags the halt with its origin so an
|
|
124
|
+
* automatic clearer (DivergenceChecker) only cancels halts it caused.
|
|
125
|
+
* Defaults to `'manual'` when omitted.
|
|
126
|
+
*/
|
|
127
|
+
setShadowWritesHalted(halted, reason, source) {
|
|
128
|
+
this.taskFlowShadowWritesHalted = halted;
|
|
129
|
+
this.taskFlowShadowWritesHaltReason = halted ? (reason ?? 'divergence-detected') : null;
|
|
130
|
+
this.taskFlowShadowWritesHaltSource = halted ? (source ?? 'manual') : null;
|
|
131
|
+
}
|
|
132
|
+
isShadowWritesHalted() {
|
|
133
|
+
return {
|
|
134
|
+
halted: this.taskFlowShadowWritesHalted,
|
|
135
|
+
reason: this.taskFlowShadowWritesHaltReason,
|
|
136
|
+
source: this.taskFlowShadowWritesHaltSource,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ── WikiClaim Evidence Phase 2 wiring ───────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Wire a SemanticMemory handle for cluster evidence emission. After this
|
|
142
|
+
* call, `addProposal()` creates a `pattern` MemoryEntity for the cluster
|
|
143
|
+
* with evidence linking to the proposal source, and `addClusterEvidence()`
|
|
144
|
+
* appends evidence atomically as more inputs join the cluster.
|
|
145
|
+
*
|
|
146
|
+
* Idempotent — safe to call multiple times; subsequent calls overwrite.
|
|
147
|
+
* Producers emit signals (evidence rows); they do NOT gate proposal flow.
|
|
148
|
+
*
|
|
149
|
+
* See docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md § Producers.
|
|
150
|
+
*/
|
|
151
|
+
setSemanticMemory(memory) {
|
|
152
|
+
this.semanticMemory = memory;
|
|
153
|
+
}
|
|
154
|
+
getSemanticMemory() {
|
|
155
|
+
return this.semanticMemory;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Append evidence to the cluster MemoryEntity for an existing proposal.
|
|
159
|
+
* Used when more feedback / commits / sessions join an open cluster.
|
|
160
|
+
* No-op when SemanticMemory is not wired or when the proposal has no
|
|
161
|
+
* cluster entity yet (legacy proposals from before Phase 2).
|
|
162
|
+
*
|
|
163
|
+
* Allowed kinds (per spec § Producers): `feedback`, `pattern-entity`,
|
|
164
|
+
* `supersedes-evidence`. Mismatches throw `EvidencePolicyError`.
|
|
165
|
+
*/
|
|
166
|
+
addClusterEvidence(proposalId, evidence) {
|
|
167
|
+
if (!this.semanticMemory)
|
|
168
|
+
return;
|
|
169
|
+
const state = this.loadEvolution();
|
|
170
|
+
const proposal = state.proposals.find((p) => p.id === proposalId);
|
|
171
|
+
if (!proposal || !proposal.entityId)
|
|
172
|
+
return;
|
|
173
|
+
// Throws EvidencePolicyError on producer/kind mismatch — surfaced to caller
|
|
174
|
+
// by design so a buggy emit-site isn't silently dropped.
|
|
175
|
+
this.semanticMemory.addEvidence(proposal.entityId, evidence, 'EvolutionManager');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Build the initial evidence array for a newly-created cluster from the
|
|
179
|
+
* proposal's `source` field. Recognized patterns (mirrors backfill rules
|
|
180
|
+
* in spec § Migration of Existing MemoryEntity Records, restricted to
|
|
181
|
+
* EvolutionManager's allowlist):
|
|
182
|
+
*
|
|
183
|
+
* - `feedback:<id>` → kind: 'feedback', sourceId: '<id>'
|
|
184
|
+
*
|
|
185
|
+
* Unrecognized sources produce no evidence (cluster is created with
|
|
186
|
+
* `evidence: []`). Subsequent `addClusterEvidence()` calls populate as
|
|
187
|
+
* inputs join. Empty evidence arrays are valid — `rememberWithEvidence`
|
|
188
|
+
* with `evidence: []` is functionally equivalent to `remember()` plus the
|
|
189
|
+
* consolidated JSONL action.
|
|
190
|
+
*/
|
|
191
|
+
buildInitialClusterEvidence(p) {
|
|
192
|
+
const now = this.now();
|
|
193
|
+
const evidence = [];
|
|
194
|
+
const feedbackMatch = p.source.match(/^feedback:(.+)$/);
|
|
195
|
+
if (feedbackMatch) {
|
|
196
|
+
evidence.push({
|
|
197
|
+
kind: 'feedback',
|
|
198
|
+
sourceId: feedbackMatch[1],
|
|
199
|
+
weight: 1.0,
|
|
200
|
+
confidence: 0.8,
|
|
201
|
+
// privacyTier omitted — inherits cluster's privacyScope ('shared-project')
|
|
202
|
+
note: p.title.slice(0, 200),
|
|
203
|
+
updatedAt: now,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return evidence;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Best-effort cluster-entity creation for a new proposal. Never throws to
|
|
210
|
+
* the caller — JSON state remains the source of truth for proposals.
|
|
211
|
+
* `EvidencePolicyError` (e.g., cap exceeded, kind mismatch) is logged and
|
|
212
|
+
* swallowed; subsequent `addClusterEvidence()` calls can recover.
|
|
213
|
+
*
|
|
214
|
+
* Returns the entity id when created, or null when memory is not wired or
|
|
215
|
+
* the cluster entity cannot be created.
|
|
216
|
+
*/
|
|
217
|
+
createClusterEntity(p) {
|
|
218
|
+
if (!this.semanticMemory)
|
|
219
|
+
return null;
|
|
220
|
+
try {
|
|
221
|
+
const evidence = this.buildInitialClusterEvidence(p);
|
|
222
|
+
const id = this.semanticMemory.rememberWithEvidence({
|
|
223
|
+
type: 'pattern',
|
|
224
|
+
name: p.title.slice(0, 200),
|
|
225
|
+
content: p.description,
|
|
226
|
+
confidence: p.impact === 'high' ? 0.85 : p.impact === 'low' ? 0.6 : 0.75,
|
|
227
|
+
lastVerified: p.proposedAt,
|
|
228
|
+
source: `evolution:${p.id}`,
|
|
229
|
+
tags: ['cluster', 'evolution', p.type, ...(p.tags ?? [])],
|
|
230
|
+
privacyScope: 'shared-project',
|
|
231
|
+
}, evidence, 'EvolutionManager');
|
|
232
|
+
return id;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
console.warn(`[EvolutionManager] cluster-entity create failed for ${p.id}:`, err instanceof Error ? err.message : err);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
taskFlowPrincipal() {
|
|
240
|
+
if (!this.taskFlowRegistry || !this.taskFlowControllerInstanceId)
|
|
241
|
+
return null;
|
|
242
|
+
return {
|
|
243
|
+
scope: 'controller',
|
|
244
|
+
controllerId: EVOLUTION_TASKFLOW_CONTROLLER_ID,
|
|
245
|
+
controllerInstanceId: this.taskFlowControllerInstanceId,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Best-effort dual-write to TaskFlow. NEVER throws to the caller — JSON
|
|
250
|
+
* remains the local durability path; TaskFlow is shadow until Phase 3b
|
|
251
|
+
* cutover. All TaskFlow errors land in console and the divergence checker
|
|
252
|
+
* picks up via state comparison.
|
|
253
|
+
*/
|
|
254
|
+
async dualWriteCreate(p) {
|
|
255
|
+
if (!this.taskFlowRegistry || this.taskFlowShadowWritesHalted)
|
|
256
|
+
return;
|
|
257
|
+
const principal = this.taskFlowPrincipal();
|
|
258
|
+
if (!principal || principal.scope !== 'controller')
|
|
259
|
+
return;
|
|
260
|
+
try {
|
|
261
|
+
await this.taskFlowRegistry.createFlow({
|
|
262
|
+
controllerId: EVOLUTION_TASKFLOW_CONTROLLER_ID,
|
|
263
|
+
controllerInstanceId: principal.controllerInstanceId,
|
|
264
|
+
ownerKey: ownerKeyForProposal(p.id),
|
|
265
|
+
idempotencyKey: `evolution-cluster-create-${p.id}`,
|
|
266
|
+
goal: p.title.slice(0, 1024),
|
|
267
|
+
currentStep: 'proposed',
|
|
268
|
+
stateJson: { proposalId: p.id, type: p.type, source: p.source },
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
// Swallow — JSON is the source of truth in Phase 3a.
|
|
273
|
+
this.logTaskFlowError('createFlow', p.id, err);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async dualWriteTransition(proposalId, action) {
|
|
277
|
+
if (!this.taskFlowRegistry || this.taskFlowShadowWritesHalted)
|
|
278
|
+
return;
|
|
279
|
+
if (action.kind === 'noop' || action.kind === 'create')
|
|
280
|
+
return;
|
|
281
|
+
const principal = this.taskFlowPrincipal();
|
|
282
|
+
if (!principal)
|
|
283
|
+
return;
|
|
284
|
+
const registry = this.taskFlowRegistry;
|
|
285
|
+
try {
|
|
286
|
+
// Look up current flow by owner key + deterministic idempotency key.
|
|
287
|
+
const existing = registry.findByIdempotency(EVOLUTION_TASKFLOW_CONTROLLER_ID, ownerKeyForProposal(proposalId), `evolution-cluster-create-${proposalId}`);
|
|
288
|
+
if (!existing) {
|
|
289
|
+
// No backfill yet — proposal exists in JSON but not TaskFlow. Skip
|
|
290
|
+
// the transition silently; the next migrate-existing pass will pick
|
|
291
|
+
// it up at its terminal state.
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const flow = registry.getFlow(existing.flowId, { bypassCache: true });
|
|
295
|
+
if (!flow)
|
|
296
|
+
return;
|
|
297
|
+
const expectedRevision = flow.revision;
|
|
298
|
+
switch (action.kind) {
|
|
299
|
+
case 'start': {
|
|
300
|
+
if (flow.status !== 'queued' && flow.status !== 'running')
|
|
301
|
+
return;
|
|
302
|
+
await registry.startStep({
|
|
303
|
+
flowId: flow.flowId,
|
|
304
|
+
expectedRevision,
|
|
305
|
+
principal,
|
|
306
|
+
currentStep: action.step,
|
|
307
|
+
});
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case 'finish': {
|
|
311
|
+
// Need flow in `running` state for finishFlow. If currently queued,
|
|
312
|
+
// promote first.
|
|
313
|
+
let rev = expectedRevision;
|
|
314
|
+
let cur = flow;
|
|
315
|
+
if (cur.status === 'queued') {
|
|
316
|
+
const r = await registry.startStep({
|
|
317
|
+
flowId: flow.flowId,
|
|
318
|
+
expectedRevision: rev,
|
|
319
|
+
principal,
|
|
320
|
+
currentStep: 'implementing',
|
|
321
|
+
});
|
|
322
|
+
cur = r.flow;
|
|
323
|
+
rev = r.flow.revision;
|
|
324
|
+
}
|
|
325
|
+
if (cur.status !== 'running')
|
|
326
|
+
return;
|
|
327
|
+
await registry.finishFlow({
|
|
328
|
+
flowId: flow.flowId,
|
|
329
|
+
expectedRevision: rev,
|
|
330
|
+
principal,
|
|
331
|
+
});
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'fail': {
|
|
335
|
+
let rev = expectedRevision;
|
|
336
|
+
let cur = flow;
|
|
337
|
+
// failFlow requires running|waiting. Promote queued → running.
|
|
338
|
+
if (cur.status === 'queued') {
|
|
339
|
+
const r = await registry.startStep({
|
|
340
|
+
flowId: flow.flowId,
|
|
341
|
+
expectedRevision: rev,
|
|
342
|
+
principal,
|
|
343
|
+
currentStep: 'reject-transition',
|
|
344
|
+
});
|
|
345
|
+
cur = r.flow;
|
|
346
|
+
rev = r.flow.revision;
|
|
347
|
+
}
|
|
348
|
+
if (cur.status !== 'running' && cur.status !== 'waiting')
|
|
349
|
+
return;
|
|
350
|
+
await registry.failFlow({
|
|
351
|
+
flowId: flow.flowId,
|
|
352
|
+
expectedRevision: rev,
|
|
353
|
+
principal,
|
|
354
|
+
failureReason: action.reason,
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case 'cancel': {
|
|
359
|
+
if (flow.status === 'succeeded' || flow.status === 'failed' ||
|
|
360
|
+
flow.status === 'cancelled' || flow.status === 'lost')
|
|
361
|
+
return;
|
|
362
|
+
// Two-phase: request, then cancel.
|
|
363
|
+
const r = await registry.requestFlowCancel({
|
|
364
|
+
flowId: flow.flowId,
|
|
365
|
+
expectedRevision,
|
|
366
|
+
requesterOrigin: { kind: 'system', id: 'EvolutionManager' },
|
|
367
|
+
});
|
|
368
|
+
await registry.cancelFlow({
|
|
369
|
+
flowId: flow.flowId,
|
|
370
|
+
expectedRevision: r.flow.revision,
|
|
371
|
+
principal,
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
this.logTaskFlowError(`transition:${action.kind}`, proposalId, err);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
logTaskFlowError(op, proposalId, err) {
|
|
382
|
+
if (err instanceof TaskFlowError) {
|
|
383
|
+
// OCC conflicts and invalid-transition errors are expected during
|
|
384
|
+
// races and concurrent updates — log at debug level only.
|
|
385
|
+
console.warn(`[EvolutionManager] taskflow ${op} for ${proposalId} skipped: ${err.code} (${err.message})`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.warn(`[EvolutionManager] taskflow ${op} for ${proposalId} unexpected error:`, err instanceof Error ? err.message : err);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Backfill all in-flight proposals (status !== 'implemented' && !== 'rejected')
|
|
393
|
+
* into TaskFlow. Idempotent via `findIdempotent` on
|
|
394
|
+
* `evolution:cluster:<proposalId>` ownerKey. Safe to call multiple times.
|
|
395
|
+
*
|
|
396
|
+
* Returns counts: created, alreadyExisted, advanced (transitioned beyond
|
|
397
|
+
* 'queued' to match the proposal's current status).
|
|
398
|
+
*/
|
|
399
|
+
async migrateExistingToTaskFlow() {
|
|
400
|
+
if (!this.taskFlowRegistry) {
|
|
401
|
+
return { created: 0, alreadyExisted: 0, advanced: 0, skipped: 0 };
|
|
402
|
+
}
|
|
403
|
+
const principal = this.taskFlowPrincipal();
|
|
404
|
+
if (!principal || principal.scope !== 'controller') {
|
|
405
|
+
return { created: 0, alreadyExisted: 0, advanced: 0, skipped: 0 };
|
|
406
|
+
}
|
|
407
|
+
const registry = this.taskFlowRegistry;
|
|
408
|
+
const state = this.loadEvolution();
|
|
409
|
+
let created = 0;
|
|
410
|
+
let alreadyExisted = 0;
|
|
411
|
+
let advanced = 0;
|
|
412
|
+
let skipped = 0;
|
|
413
|
+
for (const p of state.proposals) {
|
|
414
|
+
try {
|
|
415
|
+
const existing = registry.findByIdempotency(EVOLUTION_TASKFLOW_CONTROLLER_ID, ownerKeyForProposal(p.id), `evolution-cluster-create-${p.id}`);
|
|
416
|
+
let flow;
|
|
417
|
+
if (existing) {
|
|
418
|
+
alreadyExisted++;
|
|
419
|
+
flow = existing;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const r = await registry.createFlow({
|
|
423
|
+
controllerId: EVOLUTION_TASKFLOW_CONTROLLER_ID,
|
|
424
|
+
controllerInstanceId: principal.controllerInstanceId,
|
|
425
|
+
ownerKey: ownerKeyForProposal(p.id),
|
|
426
|
+
idempotencyKey: `evolution-cluster-create-${p.id}`,
|
|
427
|
+
goal: p.title.slice(0, 1024),
|
|
428
|
+
currentStep: 'proposed',
|
|
429
|
+
stateJson: { proposalId: p.id, type: p.type, source: p.source },
|
|
430
|
+
});
|
|
431
|
+
if (r.created)
|
|
432
|
+
created++;
|
|
433
|
+
else
|
|
434
|
+
alreadyExisted++;
|
|
435
|
+
flow = r.flow;
|
|
436
|
+
}
|
|
437
|
+
// Catch the flow up to the proposal's current status.
|
|
438
|
+
const advancedThis = await this.catchUpFlowToStatus(flow, p, principal);
|
|
439
|
+
if (advancedThis)
|
|
440
|
+
advanced++;
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
skipped++;
|
|
444
|
+
this.logTaskFlowError('migrate', p.id, err);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { created, alreadyExisted, advanced, skipped };
|
|
448
|
+
}
|
|
449
|
+
/** Catch a flow record up to the proposal's current status. */
|
|
450
|
+
async catchUpFlowToStatus(flow, p, principal) {
|
|
451
|
+
if (!this.taskFlowRegistry)
|
|
452
|
+
return false;
|
|
453
|
+
const registry = this.taskFlowRegistry;
|
|
454
|
+
let cur = registry.getFlow(flow.flowId, { bypassCache: true }) ?? flow;
|
|
455
|
+
let mutated = false;
|
|
456
|
+
const target = p.status;
|
|
457
|
+
// Already terminal — leave alone.
|
|
458
|
+
if (cur.status === 'succeeded' || cur.status === 'failed' ||
|
|
459
|
+
cur.status === 'cancelled' || cur.status === 'lost') {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
// Promote queued → running if target is running/terminal-success/failed.
|
|
463
|
+
if (cur.status === 'queued' && (target === 'approved' || target === 'in_progress' ||
|
|
464
|
+
target === 'implemented' || target === 'rejected' || target === 'deferred')) {
|
|
465
|
+
const step = target === 'in_progress' ? 'in_progress'
|
|
466
|
+
: target === 'approved' ? 'approved' : 'catch-up';
|
|
467
|
+
const r = await registry.startStep({
|
|
468
|
+
flowId: cur.flowId,
|
|
469
|
+
expectedRevision: cur.revision,
|
|
470
|
+
principal,
|
|
471
|
+
currentStep: step,
|
|
472
|
+
});
|
|
473
|
+
cur = r.flow;
|
|
474
|
+
mutated = true;
|
|
475
|
+
}
|
|
476
|
+
// Finalize.
|
|
477
|
+
if (target === 'implemented' && cur.status === 'running') {
|
|
478
|
+
await registry.finishFlow({
|
|
479
|
+
flowId: cur.flowId,
|
|
480
|
+
expectedRevision: cur.revision,
|
|
481
|
+
principal,
|
|
482
|
+
});
|
|
483
|
+
mutated = true;
|
|
484
|
+
}
|
|
485
|
+
else if (target === 'rejected' &&
|
|
486
|
+
(cur.status === 'running' || cur.status === 'waiting')) {
|
|
487
|
+
await registry.failFlow({
|
|
488
|
+
flowId: cur.flowId,
|
|
489
|
+
expectedRevision: cur.revision,
|
|
490
|
+
principal,
|
|
491
|
+
failureReason: p.resolution ?? 'rejected',
|
|
492
|
+
});
|
|
493
|
+
mutated = true;
|
|
494
|
+
}
|
|
495
|
+
else if (target === 'deferred' &&
|
|
496
|
+
(cur.status === 'running' || cur.status === 'waiting' || cur.status === 'queued')) {
|
|
497
|
+
const r = await registry.requestFlowCancel({
|
|
498
|
+
flowId: cur.flowId,
|
|
499
|
+
expectedRevision: cur.revision,
|
|
500
|
+
requesterOrigin: { kind: 'system', id: 'EvolutionManager' },
|
|
501
|
+
});
|
|
502
|
+
await registry.cancelFlow({
|
|
503
|
+
flowId: cur.flowId,
|
|
504
|
+
expectedRevision: r.flow.revision,
|
|
505
|
+
principal,
|
|
506
|
+
});
|
|
507
|
+
mutated = true;
|
|
508
|
+
}
|
|
509
|
+
return mutated;
|
|
510
|
+
}
|
|
30
511
|
/**
|
|
31
512
|
* Wire adaptive autonomy modules for runtime integration.
|
|
32
513
|
* - TrustElevationTracker: receives proposal approval/rejection events
|
|
@@ -141,8 +622,17 @@ export class EvolutionManager {
|
|
|
141
622
|
proposedAt: this.now(),
|
|
142
623
|
tags: opts.tags,
|
|
143
624
|
};
|
|
625
|
+
// WikiClaim Phase 2: create cluster MemoryEntity and capture entityId BEFORE
|
|
626
|
+
// saving JSON, so the persisted proposal carries its cluster reference.
|
|
627
|
+
// Failure to create the cluster entity is logged and swallowed inside
|
|
628
|
+
// createClusterEntity — the proposal still ships with `entityId: undefined`.
|
|
629
|
+
const entityId = this.createClusterEntity(proposal);
|
|
630
|
+
if (entityId)
|
|
631
|
+
proposal.entityId = entityId;
|
|
144
632
|
state.proposals.push(proposal);
|
|
145
633
|
this.saveEvolution(state);
|
|
634
|
+
// TaskFlow Phase 3a shadow-write — best-effort, JSON remains source of truth.
|
|
635
|
+
void this.dualWriteCreate(proposal);
|
|
146
636
|
return proposal;
|
|
147
637
|
}
|
|
148
638
|
updateProposalStatus(id, status, resolution) {
|
|
@@ -150,6 +640,7 @@ export class EvolutionManager {
|
|
|
150
640
|
const proposal = state.proposals.find(p => p.id === id);
|
|
151
641
|
if (!proposal)
|
|
152
642
|
return false;
|
|
643
|
+
const prevStatus = proposal.status;
|
|
153
644
|
proposal.status = status;
|
|
154
645
|
if (resolution)
|
|
155
646
|
proposal.resolution = resolution;
|
|
@@ -161,6 +652,9 @@ export class EvolutionManager {
|
|
|
161
652
|
const decision = status === 'approved' ? 'approved' : 'rejected';
|
|
162
653
|
this.trustElevationTracker.recordProposalDecision(proposal, decision, false);
|
|
163
654
|
}
|
|
655
|
+
// TaskFlow Phase 3a shadow-write — best-effort, JSON remains source of truth.
|
|
656
|
+
const action = statusToFlowAction(prevStatus, status, resolution);
|
|
657
|
+
void this.dualWriteTransition(id, action);
|
|
164
658
|
return true;
|
|
165
659
|
}
|
|
166
660
|
/**
|