@claude-flow/cli 3.45.0 → 3.46.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/.claude/helpers/helpers.manifest.json +2 -2
- package/README.md +2 -0
- package/bin/cli.js +13 -0
- package/bin/mcp-server.js +16 -0
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +131 -2
- package/dist/src/commands/embeddings.js +50 -7
- package/dist/src/commands/init.js +5 -2
- package/dist/src/commands/mcp.js +27 -4
- package/dist/src/mcp-server.d.ts +50 -6
- package/dist/src/mcp-server.js +267 -33
- package/dist/src/mcp-tools/agent-execute-core.js +7 -0
- package/dist/src/mcp-tools/agentdb-tools.js +20 -8
- package/dist/src/mcp-tools/browser-tools.js +30 -23
- package/dist/src/mcp-tools/memory-tools.js +9 -4
- package/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/dist/src/mcp-tools/policy-enforcer.d.ts +4 -4
- package/dist/src/mcp-tools/policy-enforcer.js +4 -4
- package/dist/src/mcp-tools/terminal-tools.js +11 -1
- package/dist/src/mcp-tools/workflow-tools.js +1 -1
- package/dist/src/memory/ewc-consolidation.d.ts +16 -1
- package/dist/src/memory/ewc-consolidation.js +63 -13
- package/dist/src/memory/intelligence.d.ts +6 -2
- package/dist/src/memory/intelligence.js +18 -11
- package/dist/src/memory/memory-bridge.js +84 -0
- package/dist/src/ruvector/model-router.js +19 -1
- package/dist/src/services/distill-oracle.js +2 -1
- package/dist/src/services/git-workspace-identity.js +5 -8
- package/dist/src/services/policy-runtime.js +114 -4
- package/dist/src/services/worker-daemon.js +8 -3
- package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js +2 -6
- package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.js +14 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts +40 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts.map +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js +100 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js.map +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -0
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/node_modules/@claude-flow/security/dist/safe-git.d.ts +22 -0
- package/node_modules/@claude-flow/security/dist/safe-git.d.ts.map +1 -0
- package/node_modules/@claude-flow/security/dist/safe-git.js +63 -0
- package/node_modules/@claude-flow/security/dist/safe-git.js.map +1 -0
- package/package.json +5 -5
- package/plugins/ruflo-metaharness/agents/metaharness-architect.md +1 -1
- package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +1 -1
- package/plugins/ruflo-metaharness/scripts/_darwin.mjs +84 -12
- package/plugins/ruflo-metaharness/scripts/_harness.mjs +10 -4
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +120 -6
- package/plugins/ruflo-metaharness/scripts/_redblue.mjs +33 -6
- package/plugins/ruflo-metaharness/scripts/audit-list.mjs +11 -10
- package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +8 -7
- package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +2 -3
- package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +8 -7
- package/plugins/ruflo-metaharness/scripts/similarity.mjs +9 -8
- package/plugins/ruflo-metaharness/scripts/smoke.sh +31 -0
- package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +319 -0
- package/plugins/ruflo-metaharness/scripts/test-pin-alignment.mjs +182 -0
- package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +4 -2
- package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +3 -1
- package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +1 -1
|
@@ -153,6 +153,16 @@ export const terminalTools = [
|
|
|
153
153
|
const startTime = Date.now();
|
|
154
154
|
let output;
|
|
155
155
|
let exitCode;
|
|
156
|
+
// dream-cycle 2026-09-21 (post-review): session.env was validated once
|
|
157
|
+
// at terminal_create time and trusted forever after via the on-disk
|
|
158
|
+
// store — a session persisted before a denylist update (or a store.json
|
|
159
|
+
// edited/restored out of band) could carry an env that the *current*
|
|
160
|
+
// validateEnv() would reject, silently bypassing the denylist for every
|
|
161
|
+
// execute on that pre-existing session. Re-validate at execution time
|
|
162
|
+
// too; drop (not fail the whole call) on now-invalid env so a stale
|
|
163
|
+
// session degrades to a clean environment rather than blocking.
|
|
164
|
+
const vSessionEnv = validateEnv(session.env, 'session.env');
|
|
165
|
+
const effectiveEnv = vSessionEnv.valid ? vSessionEnv.sanitized : {};
|
|
156
166
|
try {
|
|
157
167
|
output = execSync(command, {
|
|
158
168
|
cwd,
|
|
@@ -160,7 +170,7 @@ export const terminalTools = [
|
|
|
160
170
|
timeout,
|
|
161
171
|
maxBuffer: 5 * 1024 * 1024,
|
|
162
172
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
163
|
-
env: { ...process.env, ...
|
|
173
|
+
env: { ...process.env, ...effectiveEnv },
|
|
164
174
|
});
|
|
165
175
|
exitCode = 0;
|
|
166
176
|
}
|
|
@@ -256,7 +256,7 @@ export const workflowTools = [
|
|
|
256
256
|
const stepResults = [];
|
|
257
257
|
// Variable substitution: {{name}} → workflow.variables[name] OR steps[stepId].output
|
|
258
258
|
const interp = (text) => {
|
|
259
|
-
return text.replace(/\{\{\s*([a-zA-Z_][\w
|
|
259
|
+
return text.replace(/\{\{\s*([a-zA-Z_][\w.-]*)\s*\}\}/g, (_, key) => {
|
|
260
260
|
// Direct variable
|
|
261
261
|
if (key in workflow.variables)
|
|
262
262
|
return String(workflow.variables[key]);
|
|
@@ -212,7 +212,13 @@ export declare class EWCConsolidator {
|
|
|
212
212
|
* Called by SONA after distillLearning to track which patterns
|
|
213
213
|
* are important and should be protected from forgetting.
|
|
214
214
|
*
|
|
215
|
-
* Uses online averaging: F_new =
|
|
215
|
+
* Uses online averaging: F_new = (1 - decay) * F_old + decay * F_current
|
|
216
|
+
* (same convention as computeFisherMatrix()/recordGradient() above: decay
|
|
217
|
+
* is the weight given to the new batch, so a small decay — the default,
|
|
218
|
+
* 0.01 — means the accumulated Fisher decays slowly and survives many
|
|
219
|
+
* subsequent low-signal calls, per the standard online-EWC/EWC++ update
|
|
220
|
+
* rule, e.g. Chaudhry et al., "Riemannian Walk for Incremental Learning",
|
|
221
|
+
* ECCV 2018, arXiv:1801.10112).
|
|
216
222
|
*
|
|
217
223
|
* @param confidenceChanges - Array of {id, embedding, oldConf, newConf}
|
|
218
224
|
*/
|
|
@@ -235,6 +241,15 @@ export declare class EWCConsolidator {
|
|
|
235
241
|
* Clear all patterns and history (full reset)
|
|
236
242
|
*/
|
|
237
243
|
clear(): void;
|
|
244
|
+
/**
|
|
245
|
+
* Validate a loaded globalFisher candidate before trusting it: must be an
|
|
246
|
+
* array of exactly `dimensions` finite, non-negative numbers. Fisher
|
|
247
|
+
* values are sums of squares (see computeFisherMatrix/recordGradient/
|
|
248
|
+
* updateFisherFromConfidences above), so a negative or non-finite entry
|
|
249
|
+
* means the persisted file is corrupted or from an incompatible layout,
|
|
250
|
+
* not a legitimate state to resume from.
|
|
251
|
+
*/
|
|
252
|
+
private isValidFisherArray;
|
|
238
253
|
/**
|
|
239
254
|
* Calculate importance score for a pattern based on usage
|
|
240
255
|
*/
|
|
@@ -357,38 +357,68 @@ export class EWCConsolidator {
|
|
|
357
357
|
* Called by SONA after distillLearning to track which patterns
|
|
358
358
|
* are important and should be protected from forgetting.
|
|
359
359
|
*
|
|
360
|
-
* Uses online averaging: F_new =
|
|
360
|
+
* Uses online averaging: F_new = (1 - decay) * F_old + decay * F_current
|
|
361
|
+
* (same convention as computeFisherMatrix()/recordGradient() above: decay
|
|
362
|
+
* is the weight given to the new batch, so a small decay — the default,
|
|
363
|
+
* 0.01 — means the accumulated Fisher decays slowly and survives many
|
|
364
|
+
* subsequent low-signal calls, per the standard online-EWC/EWC++ update
|
|
365
|
+
* rule, e.g. Chaudhry et al., "Riemannian Walk for Incremental Learning",
|
|
366
|
+
* ECCV 2018, arXiv:1801.10112).
|
|
361
367
|
*
|
|
362
368
|
* @param confidenceChanges - Array of {id, embedding, oldConf, newConf}
|
|
363
369
|
*/
|
|
364
370
|
updateFisherFromConfidences(confidenceChanges) {
|
|
365
371
|
if (confidenceChanges.length === 0)
|
|
366
372
|
return;
|
|
367
|
-
const
|
|
373
|
+
const decay = this.config.fisherDecayRate;
|
|
368
374
|
const currentFisher = new Array(this.config.dimensions).fill(0);
|
|
369
375
|
let sampleCount = 0;
|
|
370
376
|
for (const change of confidenceChanges) {
|
|
371
377
|
if (!change.embedding || change.embedding.length === 0)
|
|
372
378
|
continue;
|
|
379
|
+
if (!Number.isFinite(change.oldConf) || !Number.isFinite(change.newConf))
|
|
380
|
+
continue;
|
|
373
381
|
const confDelta = Math.abs(change.newConf - change.oldConf);
|
|
374
382
|
if (confDelta === 0)
|
|
375
383
|
continue;
|
|
376
|
-
sampleCount++;
|
|
377
384
|
const len = Math.min(change.embedding.length, this.config.dimensions);
|
|
378
|
-
// Squared gradient proxy: embedding scaled by confidence change magnitude
|
|
385
|
+
// Squared gradient proxy: embedding scaled by confidence change magnitude.
|
|
386
|
+
// Validate the whole sample before folding any of it in -- a single
|
|
387
|
+
// non-finite embedding dimension (NaN/Infinity) must not silently poison
|
|
388
|
+
// the shared accumulator for every other pattern in this batch.
|
|
389
|
+
const sampleFisher = new Array(len);
|
|
390
|
+
let sampleValid = true;
|
|
379
391
|
for (let i = 0; i < len; i++) {
|
|
380
392
|
const grad = change.embedding[i] * confDelta;
|
|
381
|
-
|
|
393
|
+
const sq = grad * grad;
|
|
394
|
+
if (!Number.isFinite(sq)) {
|
|
395
|
+
sampleValid = false;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
sampleFisher[i] = sq;
|
|
382
399
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
400
|
+
if (!sampleValid)
|
|
401
|
+
continue;
|
|
402
|
+
sampleCount++;
|
|
403
|
+
for (let i = 0; i < len; i++) {
|
|
404
|
+
currentFisher[i] += sampleFisher[i];
|
|
387
405
|
}
|
|
388
406
|
}
|
|
389
|
-
//
|
|
407
|
+
// No sample contributed usable signal (all filtered out, or every delta was
|
|
408
|
+
// zero): leave globalFisher and persisted state untouched rather than
|
|
409
|
+
// decaying accumulated importance toward an empty/no-op batch.
|
|
410
|
+
if (sampleCount === 0)
|
|
411
|
+
return;
|
|
412
|
+
for (let i = 0; i < this.config.dimensions; i++) {
|
|
413
|
+
currentFisher[i] /= sampleCount;
|
|
414
|
+
}
|
|
415
|
+
// Online EMA: F_new = (1 - decay) * F_old + decay * F_current
|
|
390
416
|
for (let i = 0; i < this.config.dimensions; i++) {
|
|
391
|
-
|
|
417
|
+
const next = (1 - decay) * this.globalFisher[i] + decay * currentFisher[i];
|
|
418
|
+
// Defensive: never let a non-finite result (e.g. from a corrupted prior
|
|
419
|
+
// globalFisher[i]) propagate into the live signal -- keep the last known
|
|
420
|
+
// finite value instead of poisoning it further.
|
|
421
|
+
this.globalFisher[i] = Number.isFinite(next) ? next : this.globalFisher[i];
|
|
392
422
|
}
|
|
393
423
|
this.saveToDisk();
|
|
394
424
|
}
|
|
@@ -432,6 +462,19 @@ export class EWCConsolidator {
|
|
|
432
462
|
// ============================================================================
|
|
433
463
|
// Private Methods
|
|
434
464
|
// ============================================================================
|
|
465
|
+
/**
|
|
466
|
+
* Validate a loaded globalFisher candidate before trusting it: must be an
|
|
467
|
+
* array of exactly `dimensions` finite, non-negative numbers. Fisher
|
|
468
|
+
* values are sums of squares (see computeFisherMatrix/recordGradient/
|
|
469
|
+
* updateFisherFromConfidences above), so a negative or non-finite entry
|
|
470
|
+
* means the persisted file is corrupted or from an incompatible layout,
|
|
471
|
+
* not a legitimate state to resume from.
|
|
472
|
+
*/
|
|
473
|
+
isValidFisherArray(value) {
|
|
474
|
+
return (Array.isArray(value) &&
|
|
475
|
+
value.length === this.config.dimensions &&
|
|
476
|
+
value.every((v) => typeof v === 'number' && Number.isFinite(v) && v >= 0));
|
|
477
|
+
}
|
|
435
478
|
/**
|
|
436
479
|
* Calculate importance score for a pattern based on usage
|
|
437
480
|
*/
|
|
@@ -524,8 +567,15 @@ export class EWCConsolidator {
|
|
|
524
567
|
if (state.version !== '1.0.0') {
|
|
525
568
|
throw new Error(`Unsupported state version: ${state.version}`);
|
|
526
569
|
}
|
|
527
|
-
// Restore
|
|
528
|
-
|
|
570
|
+
// Restore the Fisher matrix, quarantining anything malformed -- wrong
|
|
571
|
+
// length, non-numeric/non-finite entries (e.g. JSON's lossy NaN/Infinity
|
|
572
|
+
// -> null round-trip), or negative values (Fisher values are sums of
|
|
573
|
+
// squares and can never legitimately be negative) -- rather than
|
|
574
|
+
// silently absorbing corrupted or truncated state into live importance
|
|
575
|
+
// tracking on restart.
|
|
576
|
+
this.globalFisher = this.isValidFisherArray(state.globalFisher)
|
|
577
|
+
? state.globalFisher
|
|
578
|
+
: new Array(this.config.dimensions).fill(0);
|
|
529
579
|
// Restore patterns
|
|
530
580
|
this.patterns.clear();
|
|
531
581
|
if (state.patterns) {
|
|
@@ -248,6 +248,7 @@ export interface UnifiedLearningStats {
|
|
|
248
248
|
signalsProcessed: number;
|
|
249
249
|
lastAdaptation: number | null;
|
|
250
250
|
source: string;
|
|
251
|
+
scope: 'project-persisted';
|
|
251
252
|
};
|
|
252
253
|
sona: {
|
|
253
254
|
trajectoriesTotal: number;
|
|
@@ -256,6 +257,8 @@ export interface UnifiedLearningStats {
|
|
|
256
257
|
avgAdaptationTimeMs: number;
|
|
257
258
|
source: string;
|
|
258
259
|
available: boolean;
|
|
260
|
+
scope: 'process-local';
|
|
261
|
+
metric: 'recent-buffered-trajectories';
|
|
259
262
|
};
|
|
260
263
|
memoryBridge: {
|
|
261
264
|
totalEntries: number;
|
|
@@ -270,8 +273,9 @@ export interface UnifiedLearningStats {
|
|
|
270
273
|
source: string;
|
|
271
274
|
};
|
|
272
275
|
consistency: {
|
|
273
|
-
|
|
274
|
-
|
|
276
|
+
/** These counters measure different lifetimes and cannot be compared. */
|
|
277
|
+
sonaTracksGlobal: null;
|
|
278
|
+
sonaTracksGlobalDelta: null;
|
|
275
279
|
notes: string[];
|
|
276
280
|
};
|
|
277
281
|
generatedAt: string;
|
|
@@ -701,12 +701,15 @@ export async function getUnifiedLearningStats() {
|
|
|
701
701
|
let sonaStats = { trajectoriesTotal: 0, patternsLearned: 0, reasoningBankSize: 0, avgAdaptationTimeMs: 0 };
|
|
702
702
|
if (sonaCoord) {
|
|
703
703
|
try {
|
|
704
|
-
const s = sonaCoord.stats
|
|
704
|
+
const s = sonaCoord.stats();
|
|
705
705
|
sonaStats = {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
706
|
+
// LocalSonaCoordinator.stats() reports trajectoryCount, a bounded
|
|
707
|
+
// recent buffer. Reading nonexistent trajectoriesTotal / processed
|
|
708
|
+
// fields made this metric permanently 0, even after learning.
|
|
709
|
+
trajectoriesTotal: s.trajectoryCount,
|
|
710
|
+
patternsLearned: 0,
|
|
711
|
+
reasoningBankSize: bank?.stats().patternCount ?? 0,
|
|
712
|
+
avgAdaptationTimeMs: s.avgAdaptationMs,
|
|
710
713
|
};
|
|
711
714
|
}
|
|
712
715
|
catch { /* SONA not yet initialised */ }
|
|
@@ -729,11 +732,12 @@ export async function getUnifiedLearningStats() {
|
|
|
729
732
|
neuralStats = nt.getNeuralStoreStats();
|
|
730
733
|
}
|
|
731
734
|
catch { /* neural module not loadable */ }
|
|
732
|
-
//
|
|
733
|
-
|
|
735
|
+
// SONA's coordinator is recreated on each process start, while globalStats
|
|
736
|
+
// is restored from disk. Their counts can differ by thousands after a
|
|
737
|
+
// restart even when both learning paths work correctly (#3198).
|
|
734
738
|
const notes = [];
|
|
735
|
-
if (sonaAvailable &&
|
|
736
|
-
notes.push(
|
|
739
|
+
if (sonaAvailable && sonaStats.trajectoriesTotal !== intel.trajectoriesRecorded) {
|
|
740
|
+
notes.push('sona.trajectoriesTotal is a process-local, bounded recent trajectory buffer; global.trajectoriesRecorded is project-persisted. These counters have different lifetimes and are not comparable.');
|
|
737
741
|
}
|
|
738
742
|
if (intel.patternsLearned > 0 && neuralStats.patternCount === 0) {
|
|
739
743
|
notes.push(`globalStats reports ${intel.patternsLearned} patterns learned but neural_patterns store is empty — pretrain has not written here, or trajectory-end isn't promoting patterns to the neural store yet`);
|
|
@@ -748,17 +752,20 @@ export async function getUnifiedLearningStats() {
|
|
|
748
752
|
signalsProcessed: intel.signalsProcessed,
|
|
749
753
|
lastAdaptation: intel.lastAdaptation,
|
|
750
754
|
source: '.claude-flow/neural/stats.json (globalStats)',
|
|
755
|
+
scope: 'project-persisted',
|
|
751
756
|
},
|
|
752
757
|
sona: {
|
|
753
758
|
...sonaStats,
|
|
754
759
|
source: 'sonaCoordinator (in-memory, resets per process)',
|
|
755
760
|
available: sonaAvailable,
|
|
761
|
+
scope: 'process-local',
|
|
762
|
+
metric: 'recent-buffered-trajectories',
|
|
756
763
|
},
|
|
757
764
|
memoryBridge: bridgeStats,
|
|
758
765
|
neuralPatterns: neuralStats,
|
|
759
766
|
consistency: {
|
|
760
|
-
sonaTracksGlobal:
|
|
761
|
-
sonaTracksGlobalDelta,
|
|
767
|
+
sonaTracksGlobal: null,
|
|
768
|
+
sonaTracksGlobalDelta: null,
|
|
762
769
|
notes,
|
|
763
770
|
},
|
|
764
771
|
generatedAt: new Date().toISOString(),
|
|
@@ -565,6 +565,89 @@ async function cacheGet(registry, cacheKey) {
|
|
|
565
565
|
return null;
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Cross-process cache coherence.
|
|
570
|
+
*
|
|
571
|
+
* The TieredCache lives in this process, but the database file is shared:
|
|
572
|
+
* Claude Code, Codex and Grok each run their own `mcp start` against the
|
|
573
|
+
* same `.swarm/` store, and each process only invalidates entries on its
|
|
574
|
+
* OWN writes. A key another process updated or deleted kept coming back
|
|
575
|
+
* from this process's cache — old value, `found: true` — until the cached
|
|
576
|
+
* entry expired (TieredCache TTL, 5 minutes by default; reads don't extend
|
|
577
|
+
* it).
|
|
578
|
+
*
|
|
579
|
+
* SQLite's `PRAGMA data_version` changes whenever a DIFFERENT connection has
|
|
580
|
+
* committed to the database file since this connection last asked, and never
|
|
581
|
+
* for this connection's own writes (https://sqlite.org/pragma.html#pragma_data_version).
|
|
582
|
+
* Checking it before a cached read is an exact "has anyone else written?"
|
|
583
|
+
* test that costs one pragma call. When it moves, drop the whole cache:
|
|
584
|
+
* every cached shape was derived from the old database state. The first
|
|
585
|
+
* check on a handle records its baseline and clears: a no-op for a fresh
|
|
586
|
+
* handle, and the right thing if a handle is ever replaced under a live
|
|
587
|
+
* cache.
|
|
588
|
+
*
|
|
589
|
+
* Native better-sqlite3 handles only. agentdb's sql.js fallback works on a
|
|
590
|
+
* private in-memory image that never sees another process's commits, so
|
|
591
|
+
* there is nothing to detect; it is skipped rather than probed. A handle
|
|
592
|
+
* that cannot prepare the check is remembered and never probed again; a
|
|
593
|
+
* check that fails on a given read (e.g. SQLITE_BUSY) clears the cache for
|
|
594
|
+
* safety and is simply retried on the next read.
|
|
595
|
+
*/
|
|
596
|
+
const _cacheDataVersionStmts = new WeakMap();
|
|
597
|
+
const _cacheDataVersions = new WeakMap();
|
|
598
|
+
const _cacheDataVersionUnsupported = new WeakSet();
|
|
599
|
+
function dropCacheIfDbChangedElsewhere(registry, ctx) {
|
|
600
|
+
// Never let this check break the read it guards: any throw falls back to
|
|
601
|
+
// the pre-fix behaviour (a possibly stale hit).
|
|
602
|
+
try {
|
|
603
|
+
const db = ctx.db;
|
|
604
|
+
if (!db || ctx.agentdb?.isWasm === true || !('inTransaction' in db))
|
|
605
|
+
return;
|
|
606
|
+
if (_cacheDataVersionUnsupported.has(db))
|
|
607
|
+
return;
|
|
608
|
+
let stmt = _cacheDataVersionStmts.get(db);
|
|
609
|
+
if (!stmt) {
|
|
610
|
+
try {
|
|
611
|
+
stmt = db.prepare('PRAGMA data_version');
|
|
612
|
+
_cacheDataVersionStmts.set(db, stmt);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
_cacheDataVersionUnsupported.add(db); // structural: this handle can't answer
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
let cache = null;
|
|
620
|
+
try {
|
|
621
|
+
cache = registry.get('tieredCache');
|
|
622
|
+
}
|
|
623
|
+
catch { /* no cache, nothing to drop */ }
|
|
624
|
+
try {
|
|
625
|
+
const version = stmt.get()?.data_version;
|
|
626
|
+
if (typeof version !== 'number') {
|
|
627
|
+
// #3360: an unusable answer is not evidence the store is unchanged.
|
|
628
|
+
// Same posture as the catch below — a check we cannot validate drops
|
|
629
|
+
// the cache rather than trusting it, and leaves the baseline alone so
|
|
630
|
+
// the next read re-checks.
|
|
631
|
+
if (cache && typeof cache.clear === 'function')
|
|
632
|
+
cache.clear();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (_cacheDataVersions.get(db) === version)
|
|
636
|
+
return;
|
|
637
|
+
if (cache && typeof cache.clear === 'function')
|
|
638
|
+
cache.clear();
|
|
639
|
+
_cacheDataVersions.set(db, version);
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
// Transient (e.g. SQLITE_BUSY): don't trust the cache for this read.
|
|
643
|
+
if (cache && typeof cache.clear === 'function')
|
|
644
|
+
cache.clear();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// Non-fatal.
|
|
649
|
+
}
|
|
650
|
+
}
|
|
568
651
|
/**
|
|
569
652
|
* Write to TieredCache after DB write.
|
|
570
653
|
*/
|
|
@@ -1323,6 +1406,7 @@ export async function bridgeGetEntry(options) {
|
|
|
1323
1406
|
const safeNs = String(namespace).replace(/:/g, '_');
|
|
1324
1407
|
const safeKey = String(key).replace(/:/g, '_');
|
|
1325
1408
|
const cacheKey = `entry:${safeNs}:${safeKey}`;
|
|
1409
|
+
dropCacheIfDbChangedElsewhere(registry, ctx);
|
|
1326
1410
|
const cached = await cacheGet(registry, cacheKey);
|
|
1327
1411
|
if (cached && cached.content) {
|
|
1328
1412
|
return {
|
|
@@ -296,6 +296,24 @@ function envMaxUncertainty() {
|
|
|
296
296
|
return undefined;
|
|
297
297
|
return n;
|
|
298
298
|
}
|
|
299
|
+
// Dream Cycle 2026-09-17 — env override for priorDecay, mirroring
|
|
300
|
+
// envMaxUncertainty() above. The discounted-Thompson-sampling primitive
|
|
301
|
+
// (see priorDecay's doc comment) was built, tested, and benchmarked in
|
|
302
|
+
// #3049 but shipped with no way to actually turn it on — DEFAULT_CONFIG
|
|
303
|
+
// hardcoded 1 (disabled) with no env/config knob, unlike every sibling
|
|
304
|
+
// tunable in this file. Parsed once at module load; invalid/out-of-range
|
|
305
|
+
// values fall through to the constructor's own priorDecay<=0||>1 guard,
|
|
306
|
+
// which already normalizes to 1 (disabled) — so an unset or malformed env
|
|
307
|
+
// var is fully backward compatible with existing deployments.
|
|
308
|
+
function envPriorDecay() {
|
|
309
|
+
const raw = process.env.CLAUDE_FLOW_PRIOR_DECAY;
|
|
310
|
+
if (!raw)
|
|
311
|
+
return undefined;
|
|
312
|
+
const n = Number(raw);
|
|
313
|
+
if (!Number.isFinite(n) || n <= 0 || n > 1)
|
|
314
|
+
return undefined;
|
|
315
|
+
return n;
|
|
316
|
+
}
|
|
299
317
|
const DEFAULT_CONFIG = {
|
|
300
318
|
confidenceThreshold: 0.85,
|
|
301
319
|
maxUncertainty: envMaxUncertainty() ?? 0.15,
|
|
@@ -305,7 +323,7 @@ const DEFAULT_CONFIG = {
|
|
|
305
323
|
autoSaveInterval: 1, // Save after every decision for CLI persistence
|
|
306
324
|
enableCostOptimization: true,
|
|
307
325
|
preferSpeed: true,
|
|
308
|
-
priorDecay: 1,
|
|
326
|
+
priorDecay: envPriorDecay() ?? 1,
|
|
309
327
|
};
|
|
310
328
|
// Beta shape parameters must stay well clear of 0 for sampleGamma/sampleBeta
|
|
311
329
|
// to remain numerically stable across a persisted, long-running state file —
|
|
@@ -43,7 +43,8 @@ import { FableHarness, } from './fable-harness.js';
|
|
|
43
43
|
// behavior mid-run (the #142 pin-drift failure mode). Pin it, and let
|
|
44
44
|
// scripts/check-metaharness-pins.mjs watch this constant for drift. Kept in
|
|
45
45
|
// lock-step with the optionalDependencies pin in package.json and the plugin
|
|
46
|
-
// darwin cache (versioned by the plugin's own `~0.
|
|
46
|
+
// darwin cache (versioned by the plugin's own `~0.10.2` pin in _darwin.mjs;
|
|
47
|
+
// both are checked by scripts/check-metaharness-pins.mjs — #3366).
|
|
47
48
|
export const MH_DARWIN_PIN = '0.10.2';
|
|
48
49
|
// ── Public API ───────────────────────────────────────────────────────────
|
|
49
50
|
/**
|
|
@@ -21,20 +21,17 @@
|
|
|
21
21
|
* of the resolved directory path (prefixed `dir:`-style via isGit=false), so
|
|
22
22
|
* callers never need a special case.
|
|
23
23
|
*/
|
|
24
|
-
import { execFileSync } from 'child_process';
|
|
25
24
|
import { createHash } from 'crypto';
|
|
26
25
|
import { resolve } from 'path';
|
|
27
26
|
import * as fs from 'fs';
|
|
27
|
+
import { safeGitTextSync } from '@claude-flow/security/safe-git';
|
|
28
28
|
const GIT_TIMEOUT_MS = 3000;
|
|
29
29
|
function git(cwd, ...args) {
|
|
30
30
|
try {
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
36
|
-
windowsHide: true,
|
|
37
|
-
}).trim();
|
|
31
|
+
return safeGitTextSync(cwd, args, {
|
|
32
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
33
|
+
stderr: 'ignore',
|
|
34
|
+
});
|
|
38
35
|
}
|
|
39
36
|
catch {
|
|
40
37
|
return null;
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import { AgenticPolicyEngine, createLegacyCompatibleState, isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security';
|
|
2
2
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
|
-
import { userInfo } from 'node:os';
|
|
6
|
+
import { hostname, userInfo } from 'node:os';
|
|
7
7
|
const POLICY_DIR = join('.claude-flow', 'policy');
|
|
8
8
|
const POLICY_FILE = 'state.json';
|
|
9
9
|
const LOCK_FILE = 'state.lock';
|
|
10
10
|
const LOCK_STALE_MS = 30_000;
|
|
11
11
|
const LOCK_WAIT_MS = 5_000;
|
|
12
|
+
// A lock is ~170 bytes plus the hostname (at most 255 bytes).
|
|
13
|
+
const LOCK_MAX_BYTES = 1_024;
|
|
14
|
+
// Linux PID namespace of this process, e.g. `pid:[4026531836]`; undefined on
|
|
15
|
+
// other platforms or without /proc. Recorded in the lock because a pid only
|
|
16
|
+
// identifies a process inside the namespace that wrote it.
|
|
17
|
+
const PID_NAMESPACE = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return readlinkSync('/proc/self/ns/pid');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
// Linux boot id: one uuid per kernel boot, shared by every process on that
|
|
26
|
+
// kernel and readable inside containers. Recorded because `host` is only a
|
|
27
|
+
// name: two machines that share a project directory can report the same
|
|
28
|
+
// hostname, and the init PID namespace has the same inode on every Linux host.
|
|
29
|
+
const BOOT_ID = (() => {
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
12
37
|
function paths(projectRoot) {
|
|
13
38
|
const root = resolve(projectRoot);
|
|
14
39
|
const dir = join(root, POLICY_DIR);
|
|
@@ -17,12 +42,96 @@ function paths(projectRoot) {
|
|
|
17
42
|
function sleep(ms) {
|
|
18
43
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
19
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Signal-0 existence probe, as in #1799 (`isPidAlive` in swarm-tools.ts), but
|
|
47
|
+
* only ESRCH counts as dead (as `processIsAlive` in helper-refresh.ts): here a
|
|
48
|
+
* false "dead" breaks mutual exclusion, so EPERM (another user's process) and
|
|
49
|
+
* any unexpected error mean alive.
|
|
50
|
+
*/
|
|
51
|
+
function isPidAlive(pid) {
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return e.code !== 'ESRCH';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reads a lock only if it is a small regular file: never follows a symlink,
|
|
62
|
+
* never blocks on a FIFO or device, never reads more than LOCK_MAX_BYTES.
|
|
63
|
+
* Anything else returns undefined and the caller keeps the mtime rule.
|
|
64
|
+
*/
|
|
65
|
+
function readLockFile(lockPath) {
|
|
66
|
+
if (!lstatSync(lockPath).isFile())
|
|
67
|
+
return undefined;
|
|
68
|
+
const fd = openSync(lockPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
|
|
69
|
+
try {
|
|
70
|
+
const stat = fstatSync(fd);
|
|
71
|
+
if (!stat.isFile() || stat.size > LOCK_MAX_BYTES)
|
|
72
|
+
return undefined;
|
|
73
|
+
const buffer = Buffer.alloc(LOCK_MAX_BYTES);
|
|
74
|
+
return buffer.toString('utf8', 0, readSync(fd, buffer, 0, LOCK_MAX_BYTES, 0));
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A process killed inside a transaction (SIGKILL, OOM, or a signal whose
|
|
82
|
+
* default action terminates it) never runs `finally { release() }`, so every
|
|
83
|
+
* caller, MCP reads included, waited LOCK_WAIT_MS and failed with
|
|
84
|
+
* policy-state-lock-timeout until the orphan's mtime crossed LOCK_STALE_MS.
|
|
85
|
+
* The pid is trusted only if the lock was written by this OS, on this kernel
|
|
86
|
+
* boot and in this PID namespace: anywhere else (a bwrap --unshare-pid
|
|
87
|
+
* sandbox, a container, another machine on a shared filesystem) a live owner
|
|
88
|
+
* also answers ESRCH. Locks without that identity (older ruflo, a crash
|
|
89
|
+
* between create and write, anything unparsable) return false and keep the
|
|
90
|
+
* mtime rule.
|
|
91
|
+
*/
|
|
92
|
+
function lockOwnerIsDead(lockPath) {
|
|
93
|
+
try {
|
|
94
|
+
const read = readLockFile(lockPath);
|
|
95
|
+
if (read === undefined)
|
|
96
|
+
return false;
|
|
97
|
+
const owner = JSON.parse(read);
|
|
98
|
+
if (!owner || owner.host !== hostname() || owner.platform !== process.platform
|
|
99
|
+
|| owner.pidns !== PID_NAMESPACE || owner.boot !== BOOT_ID)
|
|
100
|
+
return false;
|
|
101
|
+
// Without /proc, one Linux kernel boot or PID namespace cannot be told
|
|
102
|
+
// from another, and `host` is only a name.
|
|
103
|
+
if (process.platform === 'linux'
|
|
104
|
+
&& (PID_NAMESPACE === undefined || BOOT_ID === undefined))
|
|
105
|
+
return false;
|
|
106
|
+
const pid = owner.pid;
|
|
107
|
+
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0 || pid > 0x7fffffff
|
|
108
|
+
|| isPidAlive(pid))
|
|
109
|
+
return false;
|
|
110
|
+
// The lock can change hands while it is read and probed (its owner
|
|
111
|
+
// releases and exits, another process acquires). Re-read it and answer
|
|
112
|
+
// only if it is byte for byte the lock that was read: the next owner's
|
|
113
|
+
// lock always differs in `pid` or `acquiredAt`. `dev`/`ino`/`ctime` cannot
|
|
114
|
+
// stand in for that — ext4 reuses a freed inode number at once, and before
|
|
115
|
+
// Linux 6.13 two files created in one clock tick share a ctime.
|
|
116
|
+
return readLockFile(lockPath) === read;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
20
122
|
async function acquireLock(lockPath) {
|
|
21
123
|
const started = Date.now();
|
|
22
124
|
while (Date.now() - started < LOCK_WAIT_MS) {
|
|
23
125
|
try {
|
|
24
126
|
const fd = openSync(lockPath, 'wx', 0o600);
|
|
25
|
-
writeFileSync(fd, JSON.stringify({
|
|
127
|
+
writeFileSync(fd, JSON.stringify({
|
|
128
|
+
pid: process.pid,
|
|
129
|
+
acquiredAt: Date.now(),
|
|
130
|
+
host: hostname(),
|
|
131
|
+
platform: process.platform,
|
|
132
|
+
pidns: PID_NAMESPACE,
|
|
133
|
+
boot: BOOT_ID,
|
|
134
|
+
}));
|
|
26
135
|
closeSync(fd);
|
|
27
136
|
return () => {
|
|
28
137
|
try {
|
|
@@ -33,7 +142,8 @@ async function acquireLock(lockPath) {
|
|
|
33
142
|
}
|
|
34
143
|
catch {
|
|
35
144
|
try {
|
|
36
|
-
if (
|
|
145
|
+
if (lockOwnerIsDead(lockPath)
|
|
146
|
+
|| Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
|
|
37
147
|
unlinkSync(lockPath);
|
|
38
148
|
}
|
|
39
149
|
catch { /* another process changed the lock */ }
|
|
@@ -14,6 +14,7 @@ import { EventEmitter } from 'events';
|
|
|
14
14
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
|
|
15
15
|
import { cpus } from 'os';
|
|
16
16
|
import { join } from 'path';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
17
18
|
import { writeFileAtomic } from '../fs-secure.js';
|
|
18
19
|
import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
|
|
19
20
|
// ADR-174 M3: the consolidate worker below drives the real DISTILL/CONSOLIDATE
|
|
@@ -26,6 +27,9 @@ import { backupMemoryDb } from './memory-backup.js';
|
|
|
26
27
|
import { resolveGitWorkspaceIdentity } from './git-workspace-identity.js';
|
|
27
28
|
import { getWorkspaceLeaseRegistry } from './workspace-lease.js';
|
|
28
29
|
import { getRepoSupervisorRegistry } from './repo-supervisor.js';
|
|
30
|
+
// Daemon config is read synchronously from the constructor. In an ESM build,
|
|
31
|
+
// bare `require` is undefined, so use a module-relative require for YAML.
|
|
32
|
+
const requireYaml = createRequire(import.meta.url);
|
|
29
33
|
// Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
|
|
30
34
|
const DEFAULT_WORKERS = [
|
|
31
35
|
{ type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
|
|
@@ -368,8 +372,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
368
372
|
try {
|
|
369
373
|
// Lazy-load yaml so the daemon doesn't hard-require it; if the
|
|
370
374
|
// dep isn't installed, fall back to the previous warn-only path.
|
|
371
|
-
|
|
372
|
-
const yamlMod = require('yaml');
|
|
375
|
+
const yamlMod = requireYaml('yaml');
|
|
373
376
|
const parsed = yamlMod.parse(readFileSync(yPath, 'utf-8'));
|
|
374
377
|
if (parsed && typeof parsed === 'object') {
|
|
375
378
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -1013,7 +1016,9 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1013
1016
|
return `max age ${Math.round(ttlMs / 1000)}s reached`;
|
|
1014
1017
|
}
|
|
1015
1018
|
if (idleMs > 0) {
|
|
1016
|
-
|
|
1019
|
+
// Worker timestamps survive restarts; they cannot make this process
|
|
1020
|
+
// idle before it has been alive for a full idle window (#3194).
|
|
1021
|
+
const lastActivity = Math.max(this.lastWorkerActivityMs() ?? startedMs, startedMs);
|
|
1017
1022
|
if (now - lastActivity >= idleMs) {
|
|
1018
1023
|
return `idle for ${Math.round(idleMs / 1000)}s (no worker activity)`;
|
|
1019
1024
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/worktrees/coordinator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;
|
|
1
|
+
{"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/worktrees/coordinator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAUD,qBAAa,wBAAwB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,QAAQ,EAAE,MAAM;IAQ5B,OAAO,CACL,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,EACjD,OAAO,GAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GACvD,iBAAiB;IA+CpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB;IAkBxC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;IAYnE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE;IAgBjE,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,WAAW;CAKpB"}
|