openclaw-node-harness 2.0.3 → 2.1.0
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/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/mesh-agent.js +603 -81
- package/bin/mesh-bridge.js +340 -11
- package/bin/mesh-deploy-listener.js +119 -97
- package/bin/mesh-deploy.js +8 -0
- package/bin/mesh-task-daemon.js +1005 -40
- package/bin/mesh.js +423 -6
- package/config/claude-settings.json +95 -0
- package/config/daemon.json.template +2 -1
- package/config/git-hooks/pre-commit +13 -0
- package/config/git-hooks/pre-push +12 -0
- package/config/harness-rules.json +174 -0
- package/config/plan-templates/team-bugfix.yaml +52 -0
- package/config/plan-templates/team-deploy.yaml +50 -0
- package/config/plan-templates/team-feature.yaml +71 -0
- package/config/roles/qa-engineer.yaml +36 -0
- package/config/roles/solidity-dev.yaml +51 -0
- package/config/roles/tech-architect.yaml +36 -0
- package/config/rules/framework/solidity.md +22 -0
- package/config/rules/framework/typescript.md +21 -0
- package/config/rules/framework/unity.md +21 -0
- package/config/rules/universal/design-docs.md +18 -0
- package/config/rules/universal/git-hygiene.md +18 -0
- package/config/rules/universal/security.md +19 -0
- package/config/rules/universal/test-standards.md +19 -0
- package/identity/DELEGATION.md +6 -6
- package/install.sh +300 -8
- package/lib/circling-parser.js +119 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +59 -10
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +528 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +245 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +354 -4
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +13 -5
- package/lib/mesh-registry.js +11 -2
- package/lib/mesh-tasks.js +67 -0
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +320 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +458 -0
- package/lib/transcript-parser.mjs +292 -0
- package/mission-control/drizzle/soul_schema_update.sql +29 -0
- package/mission-control/drizzle.config.ts +1 -4
- package/mission-control/package-lock.json +1571 -83
- package/mission-control/package.json +6 -2
- package/mission-control/scripts/gen-chronology.js +3 -3
- package/mission-control/scripts/import-pipeline-v2.js +0 -16
- package/mission-control/scripts/import-pipeline.js +0 -15
- package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
- package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
- package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
- package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
- package/mission-control/src/app/api/cowork/events/route.ts +65 -0
- package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
- package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
- package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
- package/mission-control/src/app/api/diagnostics/route.ts +97 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
- package/mission-control/src/app/api/mesh/events/route.ts +95 -19
- package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
- package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
- package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
- package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
- package/mission-control/src/app/api/tasks/route.ts +21 -30
- package/mission-control/src/app/cowork/page.tsx +261 -0
- package/mission-control/src/app/diagnostics/page.tsx +385 -0
- package/mission-control/src/app/graph/page.tsx +26 -0
- package/mission-control/src/app/memory/page.tsx +1 -1
- package/mission-control/src/app/obsidian/page.tsx +36 -6
- package/mission-control/src/app/roadmap/page.tsx +24 -0
- package/mission-control/src/app/souls/page.tsx +2 -2
- package/mission-control/src/components/board/execution-config.tsx +431 -0
- package/mission-control/src/components/board/kanban-board.tsx +75 -9
- package/mission-control/src/components/board/kanban-column.tsx +135 -19
- package/mission-control/src/components/board/task-card.tsx +55 -2
- package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
- package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
- package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
- package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
- package/mission-control/src/components/cowork/role-picker.tsx +102 -0
- package/mission-control/src/components/cowork/session-card.tsx +284 -0
- package/mission-control/src/components/layout/sidebar.tsx +39 -2
- package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
- package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
- package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
- package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
- package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
- package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
- package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
- package/mission-control/src/lib/config.ts +58 -0
- package/mission-control/src/lib/db/index.ts +69 -0
- package/mission-control/src/lib/db/schema.ts +61 -3
- package/mission-control/src/lib/hooks.ts +309 -0
- package/mission-control/src/lib/memory/entities.ts +3 -2
- package/mission-control/src/lib/nats.ts +66 -1
- package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
- package/mission-control/src/lib/parsers/transcript.ts +4 -4
- package/mission-control/src/lib/scheduler.ts +12 -11
- package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
- package/mission-control/src/lib/sync/tasks.ts +23 -1
- package/mission-control/src/lib/task-id.ts +32 -0
- package/mission-control/src/lib/tts/index.ts +33 -9
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
package/lib/mesh-collab.js
CHANGED
|
@@ -29,6 +29,7 @@ const COLLAB_MODE = {
|
|
|
29
29
|
PARALLEL: 'parallel', // all nodes work simultaneously
|
|
30
30
|
SEQUENTIAL: 'sequential', // nodes take turns in order
|
|
31
31
|
REVIEW: 'review', // one leader + N reviewers
|
|
32
|
+
CIRCLING_STRATEGY: 'circling_strategy', // 1 worker + 2 reviewers, asymmetric directed rounds
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
// ── Convergence Strategies ──────────────────────────
|
|
@@ -57,7 +58,9 @@ function createSession(taskId, collabSpec) {
|
|
|
57
58
|
status: COLLAB_STATUS.RECRUITING,
|
|
58
59
|
|
|
59
60
|
// Node management
|
|
60
|
-
|
|
61
|
+
// Circling requires at least 3 nodes (1 worker + 2 reviewers).
|
|
62
|
+
// Default to 3 for circling, 2 for other modes.
|
|
63
|
+
min_nodes: collabSpec.min_nodes || (collabSpec.mode === COLLAB_MODE.CIRCLING_STRATEGY ? 3 : 2),
|
|
61
64
|
max_nodes: collabSpec.max_nodes || null, // null = unlimited
|
|
62
65
|
join_window_s: collabSpec.join_window_s || 30,
|
|
63
66
|
nodes: [],
|
|
@@ -85,10 +88,32 @@ function createSession(taskId, collabSpec) {
|
|
|
85
88
|
// Scope strategy
|
|
86
89
|
scope_strategy: collabSpec.scope_strategy || 'shared',
|
|
87
90
|
|
|
91
|
+
// Heterogeneous collab: per-node role/soul assignments (Phase E)
|
|
92
|
+
// Format: [{ soul: "blockchain-auditor", role: "solidity-dev" }, { soul: "identity-architect" }]
|
|
93
|
+
// When set, recruiting assigns specific souls to joining nodes in order.
|
|
94
|
+
// When null, all nodes run the same soul (homogeneous, backward compatible).
|
|
95
|
+
node_roles: collabSpec.node_roles || null,
|
|
96
|
+
|
|
88
97
|
// Sequential mode: turn tracking
|
|
89
98
|
turn_order: [], // node_ids in execution order
|
|
90
99
|
current_turn: null, // node_id of active node (sequential mode)
|
|
91
100
|
|
|
101
|
+
// Circling Strategy mode: asymmetric directed rounds
|
|
102
|
+
// Only populated when mode === 'circling_strategy'. null for other modes.
|
|
103
|
+
circling: collabSpec.mode === COLLAB_MODE.CIRCLING_STRATEGY ? {
|
|
104
|
+
worker_node_id: null, // assigned at recruiting close (node_roles[0])
|
|
105
|
+
reviewerA_node_id: null, // assigned at recruiting close — first non-worker
|
|
106
|
+
reviewerB_node_id: null, // assigned at recruiting close — second non-worker
|
|
107
|
+
max_subrounds: collabSpec.max_subrounds || 3,
|
|
108
|
+
current_subround: 0,
|
|
109
|
+
current_step: 0, // 0 = init, 1 = review pass, 2 = integration
|
|
110
|
+
automation_tier: collabSpec.automation_tier || 2,
|
|
111
|
+
artifacts: {}, // keyed: sr{N}_step{S}_{nodeRole}_{artifactType}
|
|
112
|
+
phase: 'init', // init | circling | finalization | complete
|
|
113
|
+
artifact_failures: {}, // { nodeId_step: count } — retry tracking per node per step
|
|
114
|
+
step_started_at: null, // ISO timestamp — set by daemon at step start, used for timeout rehydration after restart
|
|
115
|
+
} : null,
|
|
116
|
+
|
|
92
117
|
// Result (filled at completion)
|
|
93
118
|
result: null,
|
|
94
119
|
|
|
@@ -105,6 +130,10 @@ function createSession(taskId, collabSpec) {
|
|
|
105
130
|
|
|
106
131
|
// ── CollabStore (KV-backed) ─────────────────────────
|
|
107
132
|
|
|
133
|
+
// Rate-limit audit error logs: max 3 per session, then go silent
|
|
134
|
+
const _auditErrorCounts = new Map();
|
|
135
|
+
const AUDIT_ERROR_LOG_LIMIT = 3;
|
|
136
|
+
|
|
108
137
|
class CollabStore {
|
|
109
138
|
constructor(kv) {
|
|
110
139
|
this.kv = kv;
|
|
@@ -139,7 +168,14 @@ class CollabStore {
|
|
|
139
168
|
...detail,
|
|
140
169
|
});
|
|
141
170
|
await this.put(session);
|
|
142
|
-
} catch {
|
|
171
|
+
} catch (err) {
|
|
172
|
+
// Best-effort — never block on audit, but log first N failures per session
|
|
173
|
+
const count = (_auditErrorCounts.get(sessionId) || 0) + 1;
|
|
174
|
+
_auditErrorCounts.set(sessionId, count);
|
|
175
|
+
if (count <= AUDIT_ERROR_LOG_LIMIT) {
|
|
176
|
+
console.error(`[collab] audit append failed for ${sessionId}/${event}: ${err.message}${count === AUDIT_ERROR_LOG_LIMIT ? ' (suppressing further audit errors for this session)' : ''}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
143
179
|
}
|
|
144
180
|
|
|
145
181
|
/**
|
|
@@ -176,6 +212,30 @@ class CollabStore {
|
|
|
176
212
|
return sessions[0] || null;
|
|
177
213
|
}
|
|
178
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Find active sessions that contain a given node.
|
|
217
|
+
* O(sessions) single pass — avoids the O(sessions × nodes) scan
|
|
218
|
+
* that detectStalls() previously used with list() + inner find().
|
|
219
|
+
*/
|
|
220
|
+
async findActiveSessionsByNode(nodeId) {
|
|
221
|
+
const results = [];
|
|
222
|
+
const allKeys = [];
|
|
223
|
+
const keys = await this.kv.keys();
|
|
224
|
+
for await (const key of keys) {
|
|
225
|
+
allKeys.push(key);
|
|
226
|
+
}
|
|
227
|
+
for (const key of allKeys) {
|
|
228
|
+
const entry = await this.kv.get(key);
|
|
229
|
+
if (!entry || !entry.value) continue;
|
|
230
|
+
const session = JSON.parse(sc.decode(entry.value));
|
|
231
|
+
if (session.status !== COLLAB_STATUS.ACTIVE) continue;
|
|
232
|
+
if (session.nodes.some(n => n.node_id === nodeId)) {
|
|
233
|
+
results.push(session);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
|
|
179
239
|
// ── Node Management ────────────────────────────────
|
|
180
240
|
|
|
181
241
|
/**
|
|
@@ -190,7 +250,8 @@ class CollabStore {
|
|
|
190
250
|
// Check max_nodes
|
|
191
251
|
if (session.max_nodes && session.nodes.length >= session.max_nodes) return null;
|
|
192
252
|
|
|
193
|
-
// Check duplicate
|
|
253
|
+
// Check duplicate — single-threaded event loop prevents concurrent joins
|
|
254
|
+
// from interleaving between find() and push(). No mutex needed.
|
|
194
255
|
if (session.nodes.find(n => n.node_id === nodeId)) return null;
|
|
195
256
|
|
|
196
257
|
session.nodes.push({
|
|
@@ -320,6 +381,9 @@ class CollabStore {
|
|
|
320
381
|
const session = await this.get(sessionId);
|
|
321
382
|
if (!session) return null;
|
|
322
383
|
|
|
384
|
+
// Only accept reflections on active sessions
|
|
385
|
+
if (session.status !== COLLAB_STATUS.ACTIVE) return null;
|
|
386
|
+
|
|
323
387
|
const currentRound = session.rounds[session.rounds.length - 1];
|
|
324
388
|
if (!currentRound) return null;
|
|
325
389
|
|
|
@@ -335,6 +399,9 @@ class CollabStore {
|
|
|
335
399
|
vote: reflection.vote || 'continue',
|
|
336
400
|
parse_failed: reflection.parse_failed || false,
|
|
337
401
|
submitted_at: new Date().toISOString(),
|
|
402
|
+
// Circling Strategy extensions (optional, backward compatible)
|
|
403
|
+
circling_step: reflection.circling_step ?? null,
|
|
404
|
+
circling_artifacts: reflection.circling_artifacts || [], // [{ type, content }]
|
|
338
405
|
});
|
|
339
406
|
|
|
340
407
|
// Update node status
|
|
@@ -437,6 +504,278 @@ class CollabStore {
|
|
|
437
504
|
return session.current_round >= session.max_rounds;
|
|
438
505
|
}
|
|
439
506
|
|
|
507
|
+
// ── Circling Strategy: Artifact Store ──────────────
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Store a typed artifact from a circling step.
|
|
511
|
+
* Key format: sr{N}_step{S}_{nodeRole}_{artifactType}
|
|
512
|
+
*/
|
|
513
|
+
async storeArtifact(sessionId, key, content) {
|
|
514
|
+
const session = await this.get(sessionId);
|
|
515
|
+
if (!session || !session.circling) return null;
|
|
516
|
+
session.circling.artifacts[key] = content;
|
|
517
|
+
|
|
518
|
+
// Session blob size check — JetStream KV max is 1MB.
|
|
519
|
+
// Warn early so operators can plan external artifact store before hitting the wall.
|
|
520
|
+
const blobSize = Buffer.byteLength(JSON.stringify(session), 'utf8');
|
|
521
|
+
if (blobSize > 950_000) {
|
|
522
|
+
console.error(`[collab] CRITICAL: session ${sessionId} blob is ${(blobSize / 1024).toFixed(0)}KB — approaching JetStream KV 1MB limit`);
|
|
523
|
+
} else if (blobSize > 800_000) {
|
|
524
|
+
console.warn(`[collab] WARNING: session ${sessionId} blob is ${(blobSize / 1024).toFixed(0)}KB — consider external artifact store`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
await this.put(session);
|
|
529
|
+
} catch (err) {
|
|
530
|
+
// JetStream KV write failure — likely blob exceeded 1MB limit.
|
|
531
|
+
// Remove the artifact that caused the overflow and re-persist without it.
|
|
532
|
+
console.error(`[collab] storeArtifact FAILED for ${sessionId}/${key}: ${err.message}. Removing artifact and persisting without it.`);
|
|
533
|
+
delete session.circling.artifacts[key];
|
|
534
|
+
await this.put(session);
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return session;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Retrieve an artifact by key.
|
|
543
|
+
*/
|
|
544
|
+
getArtifactByKey(session, key) {
|
|
545
|
+
if (!session || !session.circling) return null;
|
|
546
|
+
return session.circling.artifacts[key] || null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Find the most recent version of an artifact type from a given role.
|
|
551
|
+
* Scans backward from (current_subround - 1) step 2 → sr0_step0 (init).
|
|
552
|
+
* Returns null if not found.
|
|
553
|
+
*/
|
|
554
|
+
getLatestArtifact(session, nodeRole, artifactType) {
|
|
555
|
+
if (!session || !session.circling) return null;
|
|
556
|
+
const { current_subround, artifacts } = session.circling;
|
|
557
|
+
|
|
558
|
+
// Scan backward through sub-rounds
|
|
559
|
+
for (let sr = current_subround; sr >= 0; sr--) {
|
|
560
|
+
// Step 2 artifacts (produced during integration/refinement)
|
|
561
|
+
if (sr > 0) {
|
|
562
|
+
const key2 = `sr${sr}_step2_${nodeRole}_${artifactType}`;
|
|
563
|
+
if (artifacts[key2] !== undefined) return artifacts[key2];
|
|
564
|
+
}
|
|
565
|
+
// Step 1 artifacts (produced during review pass)
|
|
566
|
+
if (sr > 0) {
|
|
567
|
+
const key1 = `sr${sr}_step1_${nodeRole}_${artifactType}`;
|
|
568
|
+
if (artifacts[key1] !== undefined) return artifacts[key1];
|
|
569
|
+
}
|
|
570
|
+
// Init artifacts (sr0_step0)
|
|
571
|
+
if (sr === 0) {
|
|
572
|
+
const key0 = `sr0_step0_${nodeRole}_${artifactType}`;
|
|
573
|
+
if (artifacts[key0] !== undefined) return artifacts[key0];
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Build node-specific directed input for a circling step.
|
|
581
|
+
* Uses the information flow matrix from the Circling Strategy protocol spec (§5).
|
|
582
|
+
*
|
|
583
|
+
* Returns a string containing the directed artifacts for this node at this step.
|
|
584
|
+
*/
|
|
585
|
+
compileDirectedInput(session, nodeId, taskDescription) {
|
|
586
|
+
if (!session || !session.circling) return '';
|
|
587
|
+
const { phase, current_subround, current_step } = session.circling;
|
|
588
|
+
const node = session.nodes.find(n => n.node_id === nodeId);
|
|
589
|
+
if (!node) return '';
|
|
590
|
+
|
|
591
|
+
const isWorker = nodeId === session.circling.worker_node_id;
|
|
592
|
+
const parts = [];
|
|
593
|
+
|
|
594
|
+
// Use stored reviewer IDs (assigned at recruiting close) for stable identity.
|
|
595
|
+
// Falls back to array-index computation if IDs aren't set (backward compat).
|
|
596
|
+
const reviewerLabel = (nId) => {
|
|
597
|
+
if (session.circling.reviewerA_node_id && session.circling.reviewerB_node_id) {
|
|
598
|
+
return nId === session.circling.reviewerA_node_id ? 'reviewerA' : 'reviewerB';
|
|
599
|
+
}
|
|
600
|
+
// Legacy fallback: compute from array position
|
|
601
|
+
const reviewerNodes = session.nodes.filter(n => n.node_id !== session.circling.worker_node_id);
|
|
602
|
+
const idx = reviewerNodes.findIndex(n => n.node_id === nId);
|
|
603
|
+
return idx === 0 ? 'reviewerA' : 'reviewerB';
|
|
604
|
+
};
|
|
605
|
+
const myReviewerRole = !isWorker ? reviewerLabel(nodeId) : null;
|
|
606
|
+
|
|
607
|
+
// Helper: add artifact to parts, handling null (required vs optional)
|
|
608
|
+
const addArtifact = (label, nodeRole, artifactType, required) => {
|
|
609
|
+
const content = this.getLatestArtifact(session, nodeRole, artifactType);
|
|
610
|
+
if (content !== null) {
|
|
611
|
+
parts.push(`## ${label}\n\n${content}`);
|
|
612
|
+
} else if (required) {
|
|
613
|
+
parts.push(`## ${label}\n\n[UNAVAILABLE: ${nodeRole}'s ${artifactType} was not produced — proceed with available inputs only]`);
|
|
614
|
+
}
|
|
615
|
+
// If not required and null, skip silently
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
switch (phase) {
|
|
619
|
+
case 'init':
|
|
620
|
+
parts.push(`## Task Plan\n\n${taskDescription || '(no description)'}`);
|
|
621
|
+
break;
|
|
622
|
+
|
|
623
|
+
case 'circling':
|
|
624
|
+
if (current_step === 1) {
|
|
625
|
+
// Step 1 — Review Pass
|
|
626
|
+
if (isWorker) {
|
|
627
|
+
// Worker receives: both reviewStrategies
|
|
628
|
+
addArtifact('Reviewer A Strategy', 'reviewerA', 'reviewStrategy', true);
|
|
629
|
+
addArtifact('Reviewer B Strategy', 'reviewerB', 'reviewStrategy', true);
|
|
630
|
+
// SR2+: also include reviewArtifacts so Worker can assess whether
|
|
631
|
+
// strategies are producing useful reviews (evidence alongside methodology)
|
|
632
|
+
if (current_subround > 1) {
|
|
633
|
+
addArtifact('Reviewer A — Review Findings', 'reviewerA', 'reviewArtifact', false);
|
|
634
|
+
addArtifact('Reviewer B — Review Findings', 'reviewerB', 'reviewArtifact', false);
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
// Reviewer receives: workArtifact + reconciliationDoc (optional in SR1)
|
|
638
|
+
addArtifact('Work Artifact', 'worker', 'workArtifact', true);
|
|
639
|
+
addArtifact('Reconciliation Document', 'worker', 'reconciliationDoc', current_subround > 1);
|
|
640
|
+
}
|
|
641
|
+
} else if (current_step === 2) {
|
|
642
|
+
// Step 2 — Integration + Refinement
|
|
643
|
+
if (isWorker) {
|
|
644
|
+
// Worker receives: both reviewArtifacts
|
|
645
|
+
addArtifact('Reviewer A Review', 'reviewerA', 'reviewArtifact', true);
|
|
646
|
+
addArtifact('Reviewer B Review', 'reviewerB', 'reviewArtifact', true);
|
|
647
|
+
} else {
|
|
648
|
+
// Reviewer receives: workerReviewsAnalysis + cross-review from the other reviewer.
|
|
649
|
+
// Cross-review enables inter-reviewer learning: "B caught something my
|
|
650
|
+
// methodology missed — I should incorporate that lens."
|
|
651
|
+
addArtifact('Worker Reviews Analysis', 'worker', 'workerReviewsAnalysis', true);
|
|
652
|
+
const otherReviewerRole = (myReviewerRole === 'reviewerA') ? 'reviewerB' : 'reviewerA';
|
|
653
|
+
addArtifact(`Cross-Review — ${otherReviewerRole} Findings`, otherReviewerRole, 'reviewArtifact', false);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
|
|
658
|
+
case 'finalization':
|
|
659
|
+
parts.push(`## Original Task Plan\n\n${taskDescription || '(no description)'}`);
|
|
660
|
+
addArtifact('Final Work Artifact', 'worker', 'workArtifact', true);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return parts.join('\n\n---\n\n');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Check if all nodes have submitted for the current circling step.
|
|
669
|
+
*/
|
|
670
|
+
isCirclingStepComplete(session) {
|
|
671
|
+
if (!session || !session.circling) return false;
|
|
672
|
+
const currentRound = session.rounds[session.rounds.length - 1];
|
|
673
|
+
if (!currentRound) return false;
|
|
674
|
+
|
|
675
|
+
const activeNodes = session.nodes.filter(n => n.status !== 'dead');
|
|
676
|
+
// For circling, reflections per step are tagged. Count reflections matching current step.
|
|
677
|
+
const stepReflections = currentRound.reflections.filter(
|
|
678
|
+
r => r.circling_step === session.circling.current_step
|
|
679
|
+
);
|
|
680
|
+
return stepReflections.length >= activeNodes.length;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Advance the circling state machine.
|
|
685
|
+
* Returns { phase, subround, step, needsGate } describing the new state.
|
|
686
|
+
*
|
|
687
|
+
* State machine transitions:
|
|
688
|
+
* init/step0 → circling/SR1/step1
|
|
689
|
+
* circling/step1 → circling/step2 (same SR)
|
|
690
|
+
* circling/step2 (SR < max) → circling/SR+1/step1
|
|
691
|
+
* circling/step2 (SR == max) → finalization/step0
|
|
692
|
+
*/
|
|
693
|
+
async advanceCirclingStep(sessionId) {
|
|
694
|
+
const session = await this.get(sessionId);
|
|
695
|
+
if (!session || !session.circling) return null;
|
|
696
|
+
|
|
697
|
+
const c = session.circling;
|
|
698
|
+
let needsGate = false;
|
|
699
|
+
|
|
700
|
+
if (c.phase === 'init' && c.current_step === 0) {
|
|
701
|
+
// Init complete → start circling SR1/Step1
|
|
702
|
+
c.phase = 'circling';
|
|
703
|
+
c.current_subround = 1;
|
|
704
|
+
c.current_step = 1;
|
|
705
|
+
} else if (c.phase === 'circling' && c.current_step === 1) {
|
|
706
|
+
// Step 1 complete → Step 2 (same subround)
|
|
707
|
+
c.current_step = 2;
|
|
708
|
+
} else if (c.phase === 'circling' && c.current_step === 2) {
|
|
709
|
+
// Adaptive convergence: if all active nodes voted 'converged' after step 2,
|
|
710
|
+
// skip remaining sub-rounds and go directly to finalization.
|
|
711
|
+
const currentRound = session.rounds[session.rounds.length - 1];
|
|
712
|
+
const activeNodes = session.nodes.filter(n => n.status !== 'dead');
|
|
713
|
+
const step2Reflections = currentRound
|
|
714
|
+
? currentRound.reflections.filter(r => r.circling_step === 2)
|
|
715
|
+
: [];
|
|
716
|
+
const allConverged = step2Reflections.length >= activeNodes.length &&
|
|
717
|
+
step2Reflections.every(r => r.vote === 'converged');
|
|
718
|
+
|
|
719
|
+
if (allConverged && c.current_subround < c.max_subrounds) {
|
|
720
|
+
// Early exit — all nodes agree the work is ready
|
|
721
|
+
if (c.automation_tier >= 2) {
|
|
722
|
+
needsGate = true;
|
|
723
|
+
}
|
|
724
|
+
c.phase = 'finalization';
|
|
725
|
+
c.current_step = 0;
|
|
726
|
+
} else if (c.current_subround < c.max_subrounds) {
|
|
727
|
+
// Step 2 complete, more sub-rounds → next SR/Step1
|
|
728
|
+
// Check tier gate for Tier 3 (gates after every sub-round)
|
|
729
|
+
if (c.automation_tier === 3) {
|
|
730
|
+
needsGate = true;
|
|
731
|
+
}
|
|
732
|
+
c.current_subround++;
|
|
733
|
+
c.current_step = 1;
|
|
734
|
+
} else {
|
|
735
|
+
// Final sub-round complete → finalization
|
|
736
|
+
// Tier 2 gates on finalization entry
|
|
737
|
+
if (c.automation_tier >= 2) {
|
|
738
|
+
needsGate = true;
|
|
739
|
+
}
|
|
740
|
+
c.phase = 'finalization';
|
|
741
|
+
c.current_step = 0;
|
|
742
|
+
}
|
|
743
|
+
} else if (c.phase === 'finalization') {
|
|
744
|
+
// Finalization complete → done
|
|
745
|
+
c.phase = 'complete';
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
await this.put(session);
|
|
749
|
+
return {
|
|
750
|
+
phase: c.phase,
|
|
751
|
+
subround: c.current_subround,
|
|
752
|
+
step: c.current_step,
|
|
753
|
+
needsGate,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Record an artifact parse failure for a node at the current step.
|
|
759
|
+
* Returns the failure count.
|
|
760
|
+
*/
|
|
761
|
+
async recordArtifactFailure(sessionId, nodeId) {
|
|
762
|
+
const session = await this.get(sessionId);
|
|
763
|
+
if (!session || !session.circling) return 0;
|
|
764
|
+
const key = `${nodeId}_sr${session.circling.current_subround}_step${session.circling.current_step}`;
|
|
765
|
+
session.circling.artifact_failures[key] = (session.circling.artifact_failures[key] || 0) + 1;
|
|
766
|
+
await this.put(session);
|
|
767
|
+
return session.circling.artifact_failures[key];
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Get the artifact failure count for a node at the current step.
|
|
772
|
+
*/
|
|
773
|
+
getArtifactFailureCount(session, nodeId) {
|
|
774
|
+
if (!session || !session.circling) return 0;
|
|
775
|
+
const key = `${nodeId}_sr${session.circling.current_subround}_step${session.circling.current_step}`;
|
|
776
|
+
return session.circling.artifact_failures[key] || 0;
|
|
777
|
+
}
|
|
778
|
+
|
|
440
779
|
// ── Intel Compilation ──────────────────────────────
|
|
441
780
|
|
|
442
781
|
/**
|
|
@@ -506,11 +845,14 @@ class CollabStore {
|
|
|
506
845
|
}
|
|
507
846
|
|
|
508
847
|
/**
|
|
509
|
-
* Mark session as aborted.
|
|
848
|
+
* Mark session as aborted. Returns null (no-op) if already completed or aborted.
|
|
849
|
+
* Callers can use truthiness to detect whether the abort actually happened.
|
|
510
850
|
*/
|
|
511
851
|
async markAborted(sessionId, reason) {
|
|
512
852
|
const session = await this.get(sessionId);
|
|
513
853
|
if (!session) return null;
|
|
854
|
+
// Guard: don't corrupt completed/aborted sessions
|
|
855
|
+
if (['completed', 'aborted'].includes(session.status)) return null;
|
|
514
856
|
session.status = COLLAB_STATUS.ABORTED;
|
|
515
857
|
session.completed_at = new Date().toISOString();
|
|
516
858
|
session.result = { success: false, summary: reason, aborted: true };
|
|
@@ -518,6 +860,14 @@ class CollabStore {
|
|
|
518
860
|
return session;
|
|
519
861
|
}
|
|
520
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Clear the audit error rate-limit counter for a session.
|
|
865
|
+
* Call when a session is finalized (completed/aborted) to prevent Map leak.
|
|
866
|
+
*/
|
|
867
|
+
clearAuditErrorCount(sessionId) {
|
|
868
|
+
_auditErrorCounts.delete(sessionId);
|
|
869
|
+
}
|
|
870
|
+
|
|
521
871
|
/**
|
|
522
872
|
* Get a summary of the session for reporting.
|
|
523
873
|
*/
|