chainlesschain 0.66.0 → 0.132.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/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +380 -0
- package/src/commands/agent-network.js +254 -1
- package/src/commands/audit.js +302 -0
- package/src/commands/automation.js +271 -1
- package/src/commands/bi.js +348 -0
- package/src/commands/codegen.js +224 -0
- package/src/commands/collab.js +341 -0
- package/src/commands/compliance.js +1035 -0
- package/src/commands/cowork.js +221 -0
- package/src/commands/crosschain.js +218 -0
- package/src/commands/dbevo.js +284 -0
- package/src/commands/dev.js +252 -0
- package/src/commands/did.js +358 -0
- package/src/commands/dlp.js +341 -0
- package/src/commands/encrypt.js +341 -0
- package/src/commands/evomap.js +394 -0
- package/src/commands/export.js +256 -1
- package/src/commands/federation.js +283 -0
- package/src/commands/fusion.js +258 -0
- package/src/commands/governance.js +325 -0
- package/src/commands/hardening.js +411 -0
- package/src/commands/hook.js +148 -0
- package/src/commands/import.js +252 -0
- package/src/commands/incentive.js +322 -0
- package/src/commands/inference.js +318 -0
- package/src/commands/infra.js +244 -0
- package/src/commands/instinct.js +260 -0
- package/src/commands/ipfs.js +318 -0
- package/src/commands/kg.js +387 -0
- package/src/commands/llm.js +263 -0
- package/src/commands/lowcode.js +356 -0
- package/src/commands/marketplace.js +256 -0
- package/src/commands/mcp.js +221 -0
- package/src/commands/memory.js +248 -0
- package/src/commands/multimodal.js +296 -0
- package/src/commands/nlprog.js +356 -0
- package/src/commands/note.js +244 -0
- package/src/commands/ops.js +354 -0
- package/src/commands/orchestrate.js +166 -0
- package/src/commands/org.js +277 -0
- package/src/commands/p2p.js +390 -0
- package/src/commands/perception.js +290 -0
- package/src/commands/permmem.js +251 -0
- package/src/commands/plugin-ecosystem.js +273 -0
- package/src/commands/pqc.js +393 -0
- package/src/commands/privacy.js +321 -0
- package/src/commands/quantization.js +351 -0
- package/src/commands/rcache.js +271 -0
- package/src/commands/recommend.js +340 -0
- package/src/commands/reputation.js +261 -0
- package/src/commands/runtime.js +307 -0
- package/src/commands/scim.js +262 -0
- package/src/commands/session.js +258 -0
- package/src/commands/siem.js +246 -0
- package/src/commands/skill.js +267 -1
- package/src/commands/sla.js +259 -0
- package/src/commands/social.js +256 -0
- package/src/commands/sso.js +186 -1
- package/src/commands/stress.js +230 -0
- package/src/commands/sync.js +256 -0
- package/src/commands/tech.js +338 -0
- package/src/commands/tenant.js +351 -0
- package/src/commands/terraform.js +245 -0
- package/src/commands/tokens.js +269 -0
- package/src/commands/trust.js +249 -0
- package/src/commands/wallet.js +277 -0
- package/src/commands/workflow.js +171 -0
- package/src/commands/zkp.js +335 -0
- package/src/index.js +4 -0
- package/src/lib/a2a-protocol.js +451 -0
- package/src/lib/agent-coordinator.js +325 -0
- package/src/lib/agent-network.js +387 -0
- package/src/lib/agent-router.js +395 -0
- package/src/lib/aiops.js +478 -0
- package/src/lib/app-builder.js +239 -0
- package/src/lib/audit-logger.js +379 -0
- package/src/lib/automation-engine.js +330 -0
- package/src/lib/autonomous-developer.js +350 -0
- package/src/lib/bi-engine.js +338 -0
- package/src/lib/code-agent.js +323 -0
- package/src/lib/collaboration-governance.js +364 -0
- package/src/lib/community-governance.js +436 -0
- package/src/lib/compliance-manager.js +434 -0
- package/src/lib/content-recommendation.js +469 -0
- package/src/lib/cross-chain.js +345 -0
- package/src/lib/crypto-manager.js +350 -0
- package/src/lib/dbevo.js +338 -0
- package/src/lib/decentral-infra.js +340 -0
- package/src/lib/did-manager.js +367 -0
- package/src/lib/dlp-engine.js +389 -0
- package/src/lib/evomap-federation.js +177 -0
- package/src/lib/evomap-governance.js +276 -0
- package/src/lib/federation-hardening.js +259 -0
- package/src/lib/hardening-manager.js +348 -0
- package/src/lib/hook-manager.js +380 -0
- package/src/lib/inference-network.js +330 -0
- package/src/lib/instinct-manager.js +332 -0
- package/src/lib/ipfs-storage.js +334 -0
- package/src/lib/knowledge-exporter.js +381 -0
- package/src/lib/knowledge-graph.js +432 -0
- package/src/lib/knowledge-importer.js +379 -0
- package/src/lib/llm-providers.js +391 -0
- package/src/lib/mcp-registry.js +333 -0
- package/src/lib/memory-manager.js +330 -0
- package/src/lib/multimodal.js +346 -0
- package/src/lib/nl-programming.js +343 -0
- package/src/lib/note-versioning.js +327 -0
- package/src/lib/org-manager.js +323 -0
- package/src/lib/p2p-manager.js +387 -0
- package/src/lib/perception.js +346 -0
- package/src/lib/perf-tuning.js +4 -1
- package/src/lib/permanent-memory.js +320 -0
- package/src/lib/plugin-ecosystem.js +377 -0
- package/src/lib/pqc-manager.js +368 -0
- package/src/lib/privacy-computing.js +427 -0
- package/src/lib/protocol-fusion.js +417 -0
- package/src/lib/quantization.js +325 -0
- package/src/lib/reputation-optimizer.js +299 -0
- package/src/lib/response-cache.js +327 -0
- package/src/lib/scim-manager.js +329 -0
- package/src/lib/session-manager.js +329 -0
- package/src/lib/siem-exporter.js +333 -0
- package/src/lib/skill-loader.js +377 -0
- package/src/lib/skill-marketplace.js +325 -0
- package/src/lib/sla-manager.js +275 -0
- package/src/lib/social-manager.js +326 -0
- package/src/lib/sso-manager.js +332 -0
- package/src/lib/stress-tester.js +330 -0
- package/src/lib/sync-manager.js +326 -0
- package/src/lib/tech-learning-engine.js +369 -0
- package/src/lib/tenant-saas.js +460 -0
- package/src/lib/terraform-manager.js +363 -0
- package/src/lib/threat-intel.js +335 -0
- package/src/lib/token-incentive.js +293 -0
- package/src/lib/token-tracker.js +329 -0
- package/src/lib/trust-security.js +390 -0
- package/src/lib/ueba.js +389 -0
- package/src/lib/universal-runtime.js +325 -0
- package/src/lib/wallet-manager.js +326 -0
- package/src/lib/workflow-engine.js +322 -0
- package/src/lib/zkp-engine.js +274 -0
package/src/lib/hook-manager.js
CHANGED
|
@@ -387,3 +387,383 @@ export function updateHookStats(
|
|
|
387
387
|
"UPDATE hooks SET execution_count = ?, error_count = ?, total_execution_time = ?, updated_at = datetime('now') WHERE id = ?",
|
|
388
388
|
).run(newCount, newErrorCount, newTotalTime, hookId);
|
|
389
389
|
}
|
|
390
|
+
|
|
391
|
+
// ===== V2 Surface: Hook Manager governance overlay (CLI v0.132.0) =====
|
|
392
|
+
// In-memory governance for hook profiles + execution lifecycle, independent of
|
|
393
|
+
// the legacy registerHook/executeHooks SQLite-backed path above.
|
|
394
|
+
|
|
395
|
+
export const HOOK_PROFILE_MATURITY_V2 = Object.freeze({
|
|
396
|
+
PENDING: "pending",
|
|
397
|
+
ACTIVE: "active",
|
|
398
|
+
DISABLED: "disabled",
|
|
399
|
+
RETIRED: "retired",
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
export const HOOK_EXEC_LIFECYCLE_V2 = Object.freeze({
|
|
403
|
+
QUEUED: "queued",
|
|
404
|
+
RUNNING: "running",
|
|
405
|
+
COMPLETED: "completed",
|
|
406
|
+
FAILED: "failed",
|
|
407
|
+
CANCELLED: "cancelled",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const _hookProfileTransitionsV2 = new Map([
|
|
411
|
+
[
|
|
412
|
+
HOOK_PROFILE_MATURITY_V2.PENDING,
|
|
413
|
+
new Set([
|
|
414
|
+
HOOK_PROFILE_MATURITY_V2.ACTIVE,
|
|
415
|
+
HOOK_PROFILE_MATURITY_V2.RETIRED,
|
|
416
|
+
]),
|
|
417
|
+
],
|
|
418
|
+
[
|
|
419
|
+
HOOK_PROFILE_MATURITY_V2.ACTIVE,
|
|
420
|
+
new Set([
|
|
421
|
+
HOOK_PROFILE_MATURITY_V2.DISABLED,
|
|
422
|
+
HOOK_PROFILE_MATURITY_V2.RETIRED,
|
|
423
|
+
]),
|
|
424
|
+
],
|
|
425
|
+
[
|
|
426
|
+
HOOK_PROFILE_MATURITY_V2.DISABLED,
|
|
427
|
+
new Set([
|
|
428
|
+
HOOK_PROFILE_MATURITY_V2.ACTIVE,
|
|
429
|
+
HOOK_PROFILE_MATURITY_V2.RETIRED,
|
|
430
|
+
]),
|
|
431
|
+
],
|
|
432
|
+
[HOOK_PROFILE_MATURITY_V2.RETIRED, new Set()],
|
|
433
|
+
]);
|
|
434
|
+
const _hookProfileTerminalV2 = new Set([HOOK_PROFILE_MATURITY_V2.RETIRED]);
|
|
435
|
+
|
|
436
|
+
const _hookExecTransitionsV2 = new Map([
|
|
437
|
+
[
|
|
438
|
+
HOOK_EXEC_LIFECYCLE_V2.QUEUED,
|
|
439
|
+
new Set([HOOK_EXEC_LIFECYCLE_V2.RUNNING, HOOK_EXEC_LIFECYCLE_V2.CANCELLED]),
|
|
440
|
+
],
|
|
441
|
+
[
|
|
442
|
+
HOOK_EXEC_LIFECYCLE_V2.RUNNING,
|
|
443
|
+
new Set([
|
|
444
|
+
HOOK_EXEC_LIFECYCLE_V2.COMPLETED,
|
|
445
|
+
HOOK_EXEC_LIFECYCLE_V2.FAILED,
|
|
446
|
+
HOOK_EXEC_LIFECYCLE_V2.CANCELLED,
|
|
447
|
+
]),
|
|
448
|
+
],
|
|
449
|
+
[HOOK_EXEC_LIFECYCLE_V2.COMPLETED, new Set()],
|
|
450
|
+
[HOOK_EXEC_LIFECYCLE_V2.FAILED, new Set()],
|
|
451
|
+
[HOOK_EXEC_LIFECYCLE_V2.CANCELLED, new Set()],
|
|
452
|
+
]);
|
|
453
|
+
const _hookExecTerminalV2 = new Set([
|
|
454
|
+
HOOK_EXEC_LIFECYCLE_V2.COMPLETED,
|
|
455
|
+
HOOK_EXEC_LIFECYCLE_V2.FAILED,
|
|
456
|
+
HOOK_EXEC_LIFECYCLE_V2.CANCELLED,
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
const _hookProfilesV2 = new Map();
|
|
460
|
+
const _hookExecsV2 = new Map();
|
|
461
|
+
let _maxActiveHooksPerOwnerV2 = 20;
|
|
462
|
+
let _maxPendingExecsPerHookV2 = 32;
|
|
463
|
+
let _hookIdleMsV2 = 24 * 60 * 60 * 1000;
|
|
464
|
+
let _hookExecStuckMsV2 = 60 * 1000;
|
|
465
|
+
|
|
466
|
+
function _hookPosIntV2(n, label) {
|
|
467
|
+
const v = Math.floor(Number(n));
|
|
468
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
469
|
+
throw new Error(`${label} must be positive integer`);
|
|
470
|
+
return v;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function setMaxActiveHooksPerOwnerV2(n) {
|
|
474
|
+
_maxActiveHooksPerOwnerV2 = _hookPosIntV2(n, "maxActiveHooksPerOwner");
|
|
475
|
+
}
|
|
476
|
+
export function getMaxActiveHooksPerOwnerV2() {
|
|
477
|
+
return _maxActiveHooksPerOwnerV2;
|
|
478
|
+
}
|
|
479
|
+
export function setMaxPendingExecsPerHookV2(n) {
|
|
480
|
+
_maxPendingExecsPerHookV2 = _hookPosIntV2(n, "maxPendingExecsPerHook");
|
|
481
|
+
}
|
|
482
|
+
export function getMaxPendingExecsPerHookV2() {
|
|
483
|
+
return _maxPendingExecsPerHookV2;
|
|
484
|
+
}
|
|
485
|
+
export function setHookIdleMsV2(n) {
|
|
486
|
+
_hookIdleMsV2 = _hookPosIntV2(n, "hookIdleMs");
|
|
487
|
+
}
|
|
488
|
+
export function getHookIdleMsV2() {
|
|
489
|
+
return _hookIdleMsV2;
|
|
490
|
+
}
|
|
491
|
+
export function setHookExecStuckMsV2(n) {
|
|
492
|
+
_hookExecStuckMsV2 = _hookPosIntV2(n, "hookExecStuckMs");
|
|
493
|
+
}
|
|
494
|
+
export function getHookExecStuckMsV2() {
|
|
495
|
+
return _hookExecStuckMsV2;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function _resetStateHookManagerV2() {
|
|
499
|
+
_hookProfilesV2.clear();
|
|
500
|
+
_hookExecsV2.clear();
|
|
501
|
+
_maxActiveHooksPerOwnerV2 = 20;
|
|
502
|
+
_maxPendingExecsPerHookV2 = 32;
|
|
503
|
+
_hookIdleMsV2 = 24 * 60 * 60 * 1000;
|
|
504
|
+
_hookExecStuckMsV2 = 60 * 1000;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function registerHookProfileV2({ id, owner, event, metadata } = {}) {
|
|
508
|
+
if (!id || typeof id !== "string") throw new Error("id is required");
|
|
509
|
+
if (!owner || typeof owner !== "string") throw new Error("owner is required");
|
|
510
|
+
if (_hookProfilesV2.has(id))
|
|
511
|
+
throw new Error(`hook profile ${id} already registered`);
|
|
512
|
+
const now = Date.now();
|
|
513
|
+
const p = {
|
|
514
|
+
id,
|
|
515
|
+
owner,
|
|
516
|
+
event: event || "*",
|
|
517
|
+
status: HOOK_PROFILE_MATURITY_V2.PENDING,
|
|
518
|
+
createdAt: now,
|
|
519
|
+
updatedAt: now,
|
|
520
|
+
activatedAt: null,
|
|
521
|
+
retiredAt: null,
|
|
522
|
+
lastTouchedAt: now,
|
|
523
|
+
metadata: { ...(metadata || {}) },
|
|
524
|
+
};
|
|
525
|
+
_hookProfilesV2.set(id, p);
|
|
526
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function _hookProfileCheck(from, to) {
|
|
530
|
+
const allowed = _hookProfileTransitionsV2.get(from);
|
|
531
|
+
if (!allowed || !allowed.has(to))
|
|
532
|
+
throw new Error(`invalid hook profile transition ${from} → ${to}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function _countActiveHooksByOwner(owner) {
|
|
536
|
+
let n = 0;
|
|
537
|
+
for (const p of _hookProfilesV2.values()) {
|
|
538
|
+
if (p.owner === owner && p.status === HOOK_PROFILE_MATURITY_V2.ACTIVE) n++;
|
|
539
|
+
}
|
|
540
|
+
return n;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function activateHookProfileV2(id) {
|
|
544
|
+
const p = _hookProfilesV2.get(id);
|
|
545
|
+
if (!p) throw new Error(`hook profile ${id} not found`);
|
|
546
|
+
_hookProfileCheck(p.status, HOOK_PROFILE_MATURITY_V2.ACTIVE);
|
|
547
|
+
const isRecovery = p.status === HOOK_PROFILE_MATURITY_V2.DISABLED;
|
|
548
|
+
if (!isRecovery) {
|
|
549
|
+
const active = _countActiveHooksByOwner(p.owner);
|
|
550
|
+
if (active >= _maxActiveHooksPerOwnerV2) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`max active hooks per owner (${_maxActiveHooksPerOwnerV2}) reached for ${p.owner}`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const now = Date.now();
|
|
557
|
+
p.status = HOOK_PROFILE_MATURITY_V2.ACTIVE;
|
|
558
|
+
p.updatedAt = now;
|
|
559
|
+
p.lastTouchedAt = now;
|
|
560
|
+
if (!p.activatedAt) p.activatedAt = now;
|
|
561
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function disableHookProfileV2(id) {
|
|
565
|
+
const p = _hookProfilesV2.get(id);
|
|
566
|
+
if (!p) throw new Error(`hook profile ${id} not found`);
|
|
567
|
+
_hookProfileCheck(p.status, HOOK_PROFILE_MATURITY_V2.DISABLED);
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
p.status = HOOK_PROFILE_MATURITY_V2.DISABLED;
|
|
570
|
+
p.updatedAt = now;
|
|
571
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function retireHookProfileV2(id) {
|
|
575
|
+
const p = _hookProfilesV2.get(id);
|
|
576
|
+
if (!p) throw new Error(`hook profile ${id} not found`);
|
|
577
|
+
_hookProfileCheck(p.status, HOOK_PROFILE_MATURITY_V2.RETIRED);
|
|
578
|
+
const now = Date.now();
|
|
579
|
+
p.status = HOOK_PROFILE_MATURITY_V2.RETIRED;
|
|
580
|
+
p.updatedAt = now;
|
|
581
|
+
if (!p.retiredAt) p.retiredAt = now;
|
|
582
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function touchHookProfileV2(id) {
|
|
586
|
+
const p = _hookProfilesV2.get(id);
|
|
587
|
+
if (!p) throw new Error(`hook profile ${id} not found`);
|
|
588
|
+
if (_hookProfileTerminalV2.has(p.status))
|
|
589
|
+
throw new Error(`cannot touch terminal hook profile ${id}`);
|
|
590
|
+
const now = Date.now();
|
|
591
|
+
p.lastTouchedAt = now;
|
|
592
|
+
p.updatedAt = now;
|
|
593
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function getHookProfileV2(id) {
|
|
597
|
+
const p = _hookProfilesV2.get(id);
|
|
598
|
+
if (!p) return null;
|
|
599
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function listHookProfilesV2() {
|
|
603
|
+
return [..._hookProfilesV2.values()].map((p) => ({
|
|
604
|
+
...p,
|
|
605
|
+
metadata: { ...p.metadata },
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function _countPendingExecsByHook(hookId) {
|
|
610
|
+
let n = 0;
|
|
611
|
+
for (const e of _hookExecsV2.values()) {
|
|
612
|
+
if (
|
|
613
|
+
e.hookId === hookId &&
|
|
614
|
+
(e.status === HOOK_EXEC_LIFECYCLE_V2.QUEUED ||
|
|
615
|
+
e.status === HOOK_EXEC_LIFECYCLE_V2.RUNNING)
|
|
616
|
+
)
|
|
617
|
+
n++;
|
|
618
|
+
}
|
|
619
|
+
return n;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function createHookExecV2({ id, hookId, payload, metadata } = {}) {
|
|
623
|
+
if (!id || typeof id !== "string") throw new Error("id is required");
|
|
624
|
+
if (!hookId || typeof hookId !== "string")
|
|
625
|
+
throw new Error("hookId is required");
|
|
626
|
+
if (_hookExecsV2.has(id)) throw new Error(`hook exec ${id} already exists`);
|
|
627
|
+
const hook = _hookProfilesV2.get(hookId);
|
|
628
|
+
if (!hook) throw new Error(`hook profile ${hookId} not found`);
|
|
629
|
+
const pending = _countPendingExecsByHook(hookId);
|
|
630
|
+
if (pending >= _maxPendingExecsPerHookV2) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
`max pending execs per hook (${_maxPendingExecsPerHookV2}) reached for ${hookId}`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const now = Date.now();
|
|
636
|
+
const e = {
|
|
637
|
+
id,
|
|
638
|
+
hookId,
|
|
639
|
+
payload: payload || null,
|
|
640
|
+
status: HOOK_EXEC_LIFECYCLE_V2.QUEUED,
|
|
641
|
+
createdAt: now,
|
|
642
|
+
updatedAt: now,
|
|
643
|
+
startedAt: null,
|
|
644
|
+
settledAt: null,
|
|
645
|
+
metadata: { ...(metadata || {}) },
|
|
646
|
+
};
|
|
647
|
+
_hookExecsV2.set(id, e);
|
|
648
|
+
return { ...e, metadata: { ...e.metadata } };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function _hookExecCheck(from, to) {
|
|
652
|
+
const allowed = _hookExecTransitionsV2.get(from);
|
|
653
|
+
if (!allowed || !allowed.has(to))
|
|
654
|
+
throw new Error(`invalid hook exec transition ${from} → ${to}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function startHookExecV2(id) {
|
|
658
|
+
const e = _hookExecsV2.get(id);
|
|
659
|
+
if (!e) throw new Error(`hook exec ${id} not found`);
|
|
660
|
+
_hookExecCheck(e.status, HOOK_EXEC_LIFECYCLE_V2.RUNNING);
|
|
661
|
+
const now = Date.now();
|
|
662
|
+
e.status = HOOK_EXEC_LIFECYCLE_V2.RUNNING;
|
|
663
|
+
e.updatedAt = now;
|
|
664
|
+
if (!e.startedAt) e.startedAt = now;
|
|
665
|
+
return { ...e, metadata: { ...e.metadata } };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function completeHookExecV2(id) {
|
|
669
|
+
const e = _hookExecsV2.get(id);
|
|
670
|
+
if (!e) throw new Error(`hook exec ${id} not found`);
|
|
671
|
+
_hookExecCheck(e.status, HOOK_EXEC_LIFECYCLE_V2.COMPLETED);
|
|
672
|
+
const now = Date.now();
|
|
673
|
+
e.status = HOOK_EXEC_LIFECYCLE_V2.COMPLETED;
|
|
674
|
+
e.updatedAt = now;
|
|
675
|
+
if (!e.settledAt) e.settledAt = now;
|
|
676
|
+
return { ...e, metadata: { ...e.metadata } };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export function failHookExecV2(id, reason) {
|
|
680
|
+
const e = _hookExecsV2.get(id);
|
|
681
|
+
if (!e) throw new Error(`hook exec ${id} not found`);
|
|
682
|
+
_hookExecCheck(e.status, HOOK_EXEC_LIFECYCLE_V2.FAILED);
|
|
683
|
+
const now = Date.now();
|
|
684
|
+
e.status = HOOK_EXEC_LIFECYCLE_V2.FAILED;
|
|
685
|
+
e.updatedAt = now;
|
|
686
|
+
if (!e.settledAt) e.settledAt = now;
|
|
687
|
+
if (reason) e.metadata.failReason = String(reason);
|
|
688
|
+
return { ...e, metadata: { ...e.metadata } };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function cancelHookExecV2(id, reason) {
|
|
692
|
+
const e = _hookExecsV2.get(id);
|
|
693
|
+
if (!e) throw new Error(`hook exec ${id} not found`);
|
|
694
|
+
_hookExecCheck(e.status, HOOK_EXEC_LIFECYCLE_V2.CANCELLED);
|
|
695
|
+
const now = Date.now();
|
|
696
|
+
e.status = HOOK_EXEC_LIFECYCLE_V2.CANCELLED;
|
|
697
|
+
e.updatedAt = now;
|
|
698
|
+
if (!e.settledAt) e.settledAt = now;
|
|
699
|
+
if (reason) e.metadata.cancelReason = String(reason);
|
|
700
|
+
return { ...e, metadata: { ...e.metadata } };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function getHookExecV2(id) {
|
|
704
|
+
const e = _hookExecsV2.get(id);
|
|
705
|
+
if (!e) return null;
|
|
706
|
+
return { ...e, metadata: { ...e.metadata } };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function listHookExecsV2() {
|
|
710
|
+
return [..._hookExecsV2.values()].map((e) => ({
|
|
711
|
+
...e,
|
|
712
|
+
metadata: { ...e.metadata },
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function autoDisableIdleHooksV2({ now } = {}) {
|
|
717
|
+
const t = now ?? Date.now();
|
|
718
|
+
const flipped = [];
|
|
719
|
+
for (const p of _hookProfilesV2.values()) {
|
|
720
|
+
if (
|
|
721
|
+
p.status === HOOK_PROFILE_MATURITY_V2.ACTIVE &&
|
|
722
|
+
t - p.lastTouchedAt >= _hookIdleMsV2
|
|
723
|
+
) {
|
|
724
|
+
p.status = HOOK_PROFILE_MATURITY_V2.DISABLED;
|
|
725
|
+
p.updatedAt = t;
|
|
726
|
+
flipped.push(p.id);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return { flipped, count: flipped.length };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function autoFailStuckHookExecsV2({ now } = {}) {
|
|
733
|
+
const t = now ?? Date.now();
|
|
734
|
+
const flipped = [];
|
|
735
|
+
for (const e of _hookExecsV2.values()) {
|
|
736
|
+
if (
|
|
737
|
+
e.status === HOOK_EXEC_LIFECYCLE_V2.RUNNING &&
|
|
738
|
+
e.startedAt != null &&
|
|
739
|
+
t - e.startedAt >= _hookExecStuckMsV2
|
|
740
|
+
) {
|
|
741
|
+
e.status = HOOK_EXEC_LIFECYCLE_V2.FAILED;
|
|
742
|
+
e.updatedAt = t;
|
|
743
|
+
if (!e.settledAt) e.settledAt = t;
|
|
744
|
+
e.metadata.failReason = "auto-fail-stuck";
|
|
745
|
+
flipped.push(e.id);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return { flipped, count: flipped.length };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export function getHookManagerStatsV2() {
|
|
752
|
+
const profilesByStatus = {};
|
|
753
|
+
for (const s of Object.values(HOOK_PROFILE_MATURITY_V2))
|
|
754
|
+
profilesByStatus[s] = 0;
|
|
755
|
+
for (const p of _hookProfilesV2.values()) profilesByStatus[p.status]++;
|
|
756
|
+
const execsByStatus = {};
|
|
757
|
+
for (const s of Object.values(HOOK_EXEC_LIFECYCLE_V2)) execsByStatus[s] = 0;
|
|
758
|
+
for (const e of _hookExecsV2.values()) execsByStatus[e.status]++;
|
|
759
|
+
return {
|
|
760
|
+
totalProfilesV2: _hookProfilesV2.size,
|
|
761
|
+
totalExecsV2: _hookExecsV2.size,
|
|
762
|
+
maxActiveHooksPerOwner: _maxActiveHooksPerOwnerV2,
|
|
763
|
+
maxPendingExecsPerHook: _maxPendingExecsPerHookV2,
|
|
764
|
+
hookIdleMs: _hookIdleMsV2,
|
|
765
|
+
hookExecStuckMs: _hookExecStuckMsV2,
|
|
766
|
+
profilesByStatus,
|
|
767
|
+
execsByStatus,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
@@ -404,4 +404,334 @@ export function getSchedulerStats(db) {
|
|
|
404
404
|
export function _resetState() {
|
|
405
405
|
_nodes.clear();
|
|
406
406
|
_tasks.clear();
|
|
407
|
+
_maxConcurrentTasksPerNode = INFERENCE_DEFAULT_MAX_CONCURRENT_TASKS_PER_NODE;
|
|
408
|
+
_heartbeatTimeoutMs = INFERENCE_DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ──────────────────────────────────────────────────────────
|
|
412
|
+
* V2 — Phase 67 surface (strictly additive)
|
|
413
|
+
* ────────────────────────────────────────────────────────── */
|
|
414
|
+
|
|
415
|
+
export const NODE_STATUS_V2 = NODE_STATUS;
|
|
416
|
+
export const TASK_STATUS_V2 = TASK_STATUS;
|
|
417
|
+
export const PRIVACY_MODE_V2 = PRIVACY_MODE;
|
|
418
|
+
|
|
419
|
+
export const INFERENCE_DEFAULT_MAX_CONCURRENT_TASKS_PER_NODE = 4;
|
|
420
|
+
export const INFERENCE_DEFAULT_HEARTBEAT_TIMEOUT_MS = 90000;
|
|
421
|
+
|
|
422
|
+
let _maxConcurrentTasksPerNode =
|
|
423
|
+
INFERENCE_DEFAULT_MAX_CONCURRENT_TASKS_PER_NODE;
|
|
424
|
+
let _heartbeatTimeoutMs = INFERENCE_DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
425
|
+
|
|
426
|
+
const TASK_TRANSITIONS_V2 = new Map([
|
|
427
|
+
["queued", new Set(["dispatched", "failed"])],
|
|
428
|
+
["dispatched", new Set(["running", "failed"])],
|
|
429
|
+
["running", new Set(["complete", "failed"])],
|
|
430
|
+
]);
|
|
431
|
+
const TASK_TERMINALS_V2 = new Set(["complete", "failed"]);
|
|
432
|
+
|
|
433
|
+
export function setMaxConcurrentTasksPerNode(n) {
|
|
434
|
+
if (typeof n !== "number" || Number.isNaN(n) || n < 1) {
|
|
435
|
+
throw new Error("maxConcurrentTasksPerNode must be a positive integer");
|
|
436
|
+
}
|
|
437
|
+
_maxConcurrentTasksPerNode = Math.floor(n);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function getMaxConcurrentTasksPerNode() {
|
|
441
|
+
return _maxConcurrentTasksPerNode;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function setHeartbeatTimeoutMs(ms) {
|
|
445
|
+
if (typeof ms !== "number" || Number.isNaN(ms) || ms < 1) {
|
|
446
|
+
throw new Error("heartbeatTimeoutMs must be a positive integer");
|
|
447
|
+
}
|
|
448
|
+
_heartbeatTimeoutMs = Math.floor(ms);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function getHeartbeatTimeoutMs() {
|
|
452
|
+
return _heartbeatTimeoutMs;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function getActiveTasksPerNode(nodeId) {
|
|
456
|
+
if (!nodeId) return 0;
|
|
457
|
+
let count = 0;
|
|
458
|
+
for (const t of _tasks.values()) {
|
|
459
|
+
if (
|
|
460
|
+
t.assigned_node === nodeId &&
|
|
461
|
+
(t.status === "dispatched" || t.status === "running")
|
|
462
|
+
) {
|
|
463
|
+
count += 1;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return count;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function submitTaskV2(db, { model, input, privacyMode, priority } = {}) {
|
|
470
|
+
if (!model) throw new Error("model is required");
|
|
471
|
+
const mode = privacyMode || DEFAULT_CONFIG.defaultPrivacyMode;
|
|
472
|
+
if (!VALID_PRIVACY_MODES.has(mode)) {
|
|
473
|
+
throw new Error(`Invalid privacy mode: ${mode}`);
|
|
474
|
+
}
|
|
475
|
+
const prio = Math.min(Math.max(priority || 5, 1), DEFAULT_CONFIG.maxPriority);
|
|
476
|
+
|
|
477
|
+
const id = _id();
|
|
478
|
+
const now = _now();
|
|
479
|
+
const task = {
|
|
480
|
+
id,
|
|
481
|
+
model,
|
|
482
|
+
input: input || null,
|
|
483
|
+
output: null,
|
|
484
|
+
privacy_mode: mode,
|
|
485
|
+
priority: prio,
|
|
486
|
+
assigned_node: null,
|
|
487
|
+
status: "queued",
|
|
488
|
+
duration_ms: null,
|
|
489
|
+
created_at: now,
|
|
490
|
+
completed_at: null,
|
|
491
|
+
started_at: null,
|
|
492
|
+
error_message: null,
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
db.prepare(
|
|
496
|
+
`INSERT INTO inference_tasks (id, model, input, output, privacy_mode, priority,
|
|
497
|
+
assigned_node, status, duration_ms, created_at, completed_at)
|
|
498
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
499
|
+
).run(
|
|
500
|
+
id,
|
|
501
|
+
model,
|
|
502
|
+
task.input,
|
|
503
|
+
null,
|
|
504
|
+
mode,
|
|
505
|
+
prio,
|
|
506
|
+
null,
|
|
507
|
+
"queued",
|
|
508
|
+
null,
|
|
509
|
+
now,
|
|
510
|
+
null,
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
_tasks.set(id, task);
|
|
514
|
+
return { ...task };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function _pickLeastLoadedOnline(privacyMode) {
|
|
518
|
+
let best = null;
|
|
519
|
+
let bestLoad = Infinity;
|
|
520
|
+
for (const n of _nodes.values()) {
|
|
521
|
+
if (n.status !== "online") continue;
|
|
522
|
+
const load = getActiveTasksPerNode(n.id);
|
|
523
|
+
if (load >= _maxConcurrentTasksPerNode) continue;
|
|
524
|
+
if (load < bestLoad) {
|
|
525
|
+
best = n;
|
|
526
|
+
bestLoad = load;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return best;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function dispatchTaskV2(db, taskId, { nodeId } = {}) {
|
|
533
|
+
const t = _tasks.get(taskId);
|
|
534
|
+
if (!t) throw new Error(`Unknown task: ${taskId}`);
|
|
535
|
+
if (t.status !== "queued") {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Invalid transition: ${t.status} → dispatched (must be queued)`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let target = null;
|
|
542
|
+
if (nodeId) {
|
|
543
|
+
target = _nodes.get(nodeId);
|
|
544
|
+
if (!target) throw new Error(`Unknown node: ${nodeId}`);
|
|
545
|
+
if (target.status !== "online") {
|
|
546
|
+
throw new Error(`Node ${nodeId} is not online (status=${target.status})`);
|
|
547
|
+
}
|
|
548
|
+
if (getActiveTasksPerNode(nodeId) >= _maxConcurrentTasksPerNode) {
|
|
549
|
+
throw new Error(`Max concurrent tasks reached for node ${nodeId}`);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
target = _pickLeastLoadedOnline(t.privacy_mode);
|
|
553
|
+
if (!target) throw new Error("No eligible online nodes available");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
t.status = "dispatched";
|
|
557
|
+
t.assigned_node = target.id;
|
|
558
|
+
target.task_count += 1;
|
|
559
|
+
|
|
560
|
+
db.prepare(
|
|
561
|
+
`UPDATE inference_tasks SET status = ?, assigned_node = ? WHERE id = ?`,
|
|
562
|
+
).run("dispatched", target.id, taskId);
|
|
563
|
+
db.prepare("UPDATE inference_nodes SET task_count = ? WHERE id = ?").run(
|
|
564
|
+
target.task_count,
|
|
565
|
+
target.id,
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
return { ...t };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function startTask(db, taskId) {
|
|
572
|
+
const t = _tasks.get(taskId);
|
|
573
|
+
if (!t) throw new Error(`Unknown task: ${taskId}`);
|
|
574
|
+
if (t.status !== "dispatched") {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`Invalid transition: ${t.status} → running (must be dispatched)`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
t.status = "running";
|
|
580
|
+
t.started_at = _now();
|
|
581
|
+
db.prepare("UPDATE inference_tasks SET status = ? WHERE id = ?").run(
|
|
582
|
+
"running",
|
|
583
|
+
taskId,
|
|
584
|
+
);
|
|
585
|
+
return { ...t };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function completeTaskV2(db, taskId, { output, durationMs } = {}) {
|
|
589
|
+
const t = _tasks.get(taskId);
|
|
590
|
+
if (!t) throw new Error(`Unknown task: ${taskId}`);
|
|
591
|
+
if (t.status !== "running") {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`Invalid transition: ${t.status} → complete (must be running)`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
t.status = "complete";
|
|
597
|
+
t.output = output != null ? output : null;
|
|
598
|
+
t.completed_at = _now();
|
|
599
|
+
if (durationMs != null) {
|
|
600
|
+
t.duration_ms = durationMs;
|
|
601
|
+
} else if (t.started_at) {
|
|
602
|
+
t.duration_ms = t.completed_at - t.started_at;
|
|
603
|
+
} else {
|
|
604
|
+
t.duration_ms = t.completed_at - t.created_at;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
db.prepare(
|
|
608
|
+
`UPDATE inference_tasks SET status = ?, output = ?, completed_at = ?,
|
|
609
|
+
duration_ms = ? WHERE id = ?`,
|
|
610
|
+
).run("complete", t.output, t.completed_at, t.duration_ms, taskId);
|
|
611
|
+
|
|
612
|
+
return { ...t };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function failTaskV2(db, taskId, { error } = {}) {
|
|
616
|
+
const t = _tasks.get(taskId);
|
|
617
|
+
if (!t) throw new Error(`Unknown task: ${taskId}`);
|
|
618
|
+
if (!TASK_TRANSITIONS_V2.get(t.status)?.has("failed")) {
|
|
619
|
+
throw new Error(`Invalid transition: ${t.status} → failed`);
|
|
620
|
+
}
|
|
621
|
+
t.status = "failed";
|
|
622
|
+
t.error_message = error || null;
|
|
623
|
+
t.output = error || null;
|
|
624
|
+
t.completed_at = _now();
|
|
625
|
+
|
|
626
|
+
db.prepare(
|
|
627
|
+
"UPDATE inference_tasks SET status = ?, output = ?, completed_at = ? WHERE id = ?",
|
|
628
|
+
).run("failed", t.output, t.completed_at, taskId);
|
|
629
|
+
|
|
630
|
+
return { ...t };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function setTaskStatus(db, taskId, newStatus, patch = {}) {
|
|
634
|
+
const t = _tasks.get(taskId);
|
|
635
|
+
if (!t) throw new Error(`Unknown task: ${taskId}`);
|
|
636
|
+
if (!Object.values(TASK_STATUS).includes(newStatus)) {
|
|
637
|
+
throw new Error(`Unknown status: ${newStatus}`);
|
|
638
|
+
}
|
|
639
|
+
const allowed = TASK_TRANSITIONS_V2.get(t.status);
|
|
640
|
+
if (!allowed || !allowed.has(newStatus)) {
|
|
641
|
+
throw new Error(`Invalid transition: ${t.status} → ${newStatus}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
t.status = newStatus;
|
|
645
|
+
if (patch.output !== undefined) t.output = patch.output;
|
|
646
|
+
if (patch.errorMessage !== undefined) t.error_message = patch.errorMessage;
|
|
647
|
+
if (patch.durationMs !== undefined) t.duration_ms = patch.durationMs;
|
|
648
|
+
if (patch.startedAt !== undefined) t.started_at = patch.startedAt;
|
|
649
|
+
if (newStatus === "running" && !t.started_at) t.started_at = _now();
|
|
650
|
+
if (TASK_TERMINALS_V2.has(newStatus)) {
|
|
651
|
+
t.completed_at = t.completed_at || _now();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
db.prepare(
|
|
655
|
+
`UPDATE inference_tasks SET status = ?, output = ?, completed_at = ?,
|
|
656
|
+
duration_ms = ? WHERE id = ?`,
|
|
657
|
+
).run(newStatus, t.output, t.completed_at, t.duration_ms, taskId);
|
|
658
|
+
|
|
659
|
+
return { ...t };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function autoMarkOfflineNodes(db) {
|
|
663
|
+
const cutoff = _now() - _heartbeatTimeoutMs;
|
|
664
|
+
const offlined = [];
|
|
665
|
+
for (const n of _nodes.values()) {
|
|
666
|
+
if (n.status === "offline") continue;
|
|
667
|
+
if (n.last_heartbeat < cutoff) {
|
|
668
|
+
n.status = "offline";
|
|
669
|
+
db.prepare("UPDATE inference_nodes SET status = ? WHERE id = ?").run(
|
|
670
|
+
"offline",
|
|
671
|
+
n.id,
|
|
672
|
+
);
|
|
673
|
+
offlined.push({ ...n });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return offlined;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export function findEligibleNodes({ capability, privacyMode } = {}) {
|
|
680
|
+
const _ = privacyMode; // reserved for future mode-aware routing
|
|
681
|
+
const out = [];
|
|
682
|
+
for (const n of _nodes.values()) {
|
|
683
|
+
if (n.status !== "online") continue;
|
|
684
|
+
if (getActiveTasksPerNode(n.id) >= _maxConcurrentTasksPerNode) continue;
|
|
685
|
+
if (capability) {
|
|
686
|
+
if (
|
|
687
|
+
!Array.isArray(n.capabilities) ||
|
|
688
|
+
!n.capabilities.includes(capability)
|
|
689
|
+
) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
out.push({ ...n });
|
|
694
|
+
}
|
|
695
|
+
return out.sort(
|
|
696
|
+
(a, b) => getActiveTasksPerNode(a.id) - getActiveTasksPerNode(b.id),
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function getInferenceStatsV2() {
|
|
701
|
+
const nodesByStatus = {};
|
|
702
|
+
for (const s of Object.values(NODE_STATUS)) nodesByStatus[s] = 0;
|
|
703
|
+
const tasksByStatus = {};
|
|
704
|
+
for (const s of Object.values(TASK_STATUS)) tasksByStatus[s] = 0;
|
|
705
|
+
const tasksByPrivacyMode = {};
|
|
706
|
+
for (const m of Object.values(PRIVACY_MODE)) tasksByPrivacyMode[m] = 0;
|
|
707
|
+
|
|
708
|
+
const loadPerNode = {};
|
|
709
|
+
for (const n of _nodes.values()) {
|
|
710
|
+
nodesByStatus[n.status] = (nodesByStatus[n.status] || 0) + 1;
|
|
711
|
+
loadPerNode[n.id] = getActiveTasksPerNode(n.id);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
let durSum = 0;
|
|
715
|
+
let durCount = 0;
|
|
716
|
+
for (const t of _tasks.values()) {
|
|
717
|
+
tasksByStatus[t.status] = (tasksByStatus[t.status] || 0) + 1;
|
|
718
|
+
tasksByPrivacyMode[t.privacy_mode] =
|
|
719
|
+
(tasksByPrivacyMode[t.privacy_mode] || 0) + 1;
|
|
720
|
+
if (t.status === "complete" && typeof t.duration_ms === "number") {
|
|
721
|
+
durSum += t.duration_ms;
|
|
722
|
+
durCount += 1;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
totalNodes: _nodes.size,
|
|
728
|
+
totalTasks: _tasks.size,
|
|
729
|
+
maxConcurrentTasksPerNode: _maxConcurrentTasksPerNode,
|
|
730
|
+
heartbeatTimeoutMs: _heartbeatTimeoutMs,
|
|
731
|
+
nodesByStatus,
|
|
732
|
+
tasksByStatus,
|
|
733
|
+
tasksByPrivacyMode,
|
|
734
|
+
loadPerNode,
|
|
735
|
+
avgDurationMs: durCount > 0 ? Math.round(durSum / durCount) : 0,
|
|
736
|
+
};
|
|
407
737
|
}
|