@jinn-network/client 0.1.1 → 0.1.2-canary.d6e72dfd
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/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +123 -0
- package/README.md +210 -37
- package/deployments/deployment-claim-registry-baseSepolia.json +13 -0
- package/deployments/deployment-jinn-testnet-faucet-baseSepolia-fast.json +15 -0
- package/dist/adapters/claim-registry/abi.d.ts +127 -0
- package/dist/adapters/claim-registry/abi.js +93 -0
- package/dist/adapters/claim-registry/abi.js.map +1 -0
- package/dist/adapters/claim-registry/client.d.ts +89 -0
- package/dist/adapters/claim-registry/client.js +205 -0
- package/dist/adapters/claim-registry/client.js.map +1 -0
- package/dist/adapters/mech/adapter.d.ts +1 -0
- package/dist/adapters/mech/adapter.js +75 -41
- package/dist/adapters/mech/adapter.js.map +1 -1
- package/dist/adapters/mech/contracts.d.ts +2 -0
- package/dist/adapters/mech/contracts.js +57 -7
- package/dist/adapters/mech/contracts.js.map +1 -1
- package/dist/adapters/mech/ipfs.d.ts +8 -0
- package/dist/adapters/mech/ipfs.js +12 -0
- package/dist/adapters/mech/ipfs.js.map +1 -1
- package/dist/adapters/mech/types.d.ts +20 -46
- package/dist/adapters/mech/types.js +16 -35
- package/dist/adapters/mech/types.js.map +1 -1
- package/dist/api/gather-status.d.ts +1 -0
- package/dist/api/gather-status.js +33 -1
- package/dist/api/gather-status.js.map +1 -1
- package/dist/api/portfolio-v0-build.d.ts +81 -0
- package/dist/api/portfolio-v0-build.js +141 -0
- package/dist/api/portfolio-v0-build.js.map +1 -0
- package/dist/api/portfolio-v0-doctor.d.ts +37 -0
- package/dist/api/portfolio-v0-doctor.js +123 -0
- package/dist/api/portfolio-v0-doctor.js.map +1 -0
- package/dist/api/rewards-build.js +1 -1
- package/dist/api/rewards-build.js.map +1 -1
- package/dist/api/status-build.d.ts +7 -0
- package/dist/api/status-build.js +1 -0
- package/dist/api/status-build.js.map +1 -1
- package/dist/bin/jinn-mcp.d.ts +0 -12
- package/dist/bin/jinn-mcp.js +5 -14
- package/dist/bin/jinn-mcp.js.map +1 -1
- package/dist/build-meta.json +1 -1
- package/dist/cli/commands/auth.js +115 -25
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/bootstrap.js +1 -0
- package/dist/cli/commands/bootstrap.js.map +1 -1
- package/dist/cli/commands/doctor.js +130 -14
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/fleet-scale.js +1 -0
- package/dist/cli/commands/fleet-scale.js.map +1 -1
- package/dist/cli/commands/fund-requirements.js +2 -0
- package/dist/cli/commands/fund-requirements.js.map +1 -1
- package/dist/cli/commands/intents.d.ts +17 -0
- package/dist/cli/commands/intents.js +489 -0
- package/dist/cli/commands/intents.js.map +1 -0
- package/dist/cli/commands/keys-backup.js +13 -11
- package/dist/cli/commands/keys-backup.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.js +19 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/plugin-install.js +8 -4
- package/dist/cli/commands/plugin-install.js.map +1 -1
- package/dist/cli/commands/quickstart.js +60 -4
- package/dist/cli/commands/quickstart.js.map +1 -1
- package/dist/cli/commands/rewards.js +27 -1
- package/dist/cli/commands/rewards.js.map +1 -1
- package/dist/cli/commands/submit-intent.js +108 -5
- package/dist/cli/commands/submit-intent.js.map +1 -1
- package/dist/cli/commands/version.js +1 -0
- package/dist/cli/commands/version.js.map +1 -1
- package/dist/cli/deployment-digest.js +5 -0
- package/dist/cli/deployment-digest.js.map +1 -1
- package/dist/cli/execution-context.js +1 -0
- package/dist/cli/execution-context.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/intent-registry-access.d.ts +64 -0
- package/dist/cli/intent-registry-access.js +187 -0
- package/dist/cli/intent-registry-access.js.map +1 -0
- package/dist/cli/introspection-context.js +1 -0
- package/dist/cli/introspection-context.js.map +1 -1
- package/dist/cli/password.d.ts +21 -9
- package/dist/cli/password.js +45 -24
- package/dist/cli/password.js.map +1 -1
- package/dist/config.d.ts +110 -8
- package/dist/config.js +41 -12
- package/dist/config.js.map +1 -1
- package/dist/daemon/creator.d.ts +7 -1
- package/dist/daemon/creator.js +38 -3
- package/dist/daemon/creator.js.map +1 -1
- package/dist/daemon/daemon.d.ts +43 -0
- package/dist/daemon/daemon.js +87 -2
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/earning/bootstrap.d.ts +2 -1
- package/dist/earning/bootstrap.js +72 -4
- package/dist/earning/bootstrap.js.map +1 -1
- package/dist/earning/contracts.d.ts +10 -0
- package/dist/earning/contracts.js +24 -0
- package/dist/earning/contracts.js.map +1 -1
- package/dist/earning/jinn-rewards.d.ts +9 -0
- package/dist/earning/jinn-rewards.js +7 -0
- package/dist/earning/jinn-rewards.js.map +1 -1
- package/dist/intents/prediction-apy-v0-auto.d.ts +11 -0
- package/dist/intents/prediction-apy-v0-auto.js +46 -0
- package/dist/intents/prediction-apy-v0-auto.js.map +1 -0
- package/dist/intents/prediction-apy-v0-template.d.ts +8 -0
- package/dist/intents/prediction-apy-v0-template.js +22 -0
- package/dist/intents/prediction-apy-v0-template.js.map +1 -0
- package/dist/intents/prediction-v0-auto.d.ts +53 -0
- package/dist/intents/prediction-v0-auto.js +84 -0
- package/dist/intents/prediction-v0-auto.js.map +1 -0
- package/dist/intents/prediction-v0-template.d.ts +65 -0
- package/dist/intents/prediction-v0-template.js +125 -0
- package/dist/intents/prediction-v0-template.js.map +1 -0
- package/dist/main.js +149 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/operator-server.d.ts +1 -1
- package/dist/mcp/operator-server.js +1 -1
- package/dist/preflight/claude-auth.d.ts +12 -1
- package/dist/preflight/claude-auth.js +21 -3
- package/dist/preflight/claude-auth.js.map +1 -1
- package/dist/restorer/engine/canonical-json.d.ts +18 -0
- package/dist/restorer/engine/canonical-json.js +59 -0
- package/dist/restorer/engine/canonical-json.js.map +1 -0
- package/dist/restorer/engine/claim.d.ts +69 -0
- package/dist/restorer/engine/claim.js +104 -0
- package/dist/restorer/engine/claim.js.map +1 -0
- package/dist/restorer/engine/delivery.d.ts +52 -0
- package/dist/restorer/engine/delivery.js +63 -0
- package/dist/restorer/engine/delivery.js.map +1 -0
- package/dist/restorer/engine/engine.d.ts +203 -0
- package/dist/restorer/engine/engine.js +753 -0
- package/dist/restorer/engine/engine.js.map +1 -0
- package/dist/restorer/engine/manifest-assembly.d.ts +67 -0
- package/dist/restorer/engine/manifest-assembly.js +79 -0
- package/dist/restorer/engine/manifest-assembly.js.map +1 -0
- package/dist/restorer/engine/packaging.d.ts +87 -0
- package/dist/restorer/engine/packaging.js +350 -0
- package/dist/restorer/engine/packaging.js.map +1 -0
- package/dist/restorer/engine/persistence.d.ts +170 -0
- package/dist/restorer/engine/persistence.js +381 -0
- package/dist/restorer/engine/persistence.js.map +1 -0
- package/dist/restorer/engine/recovery.d.ts +22 -0
- package/dist/restorer/engine/recovery.js +24 -0
- package/dist/restorer/engine/recovery.js.map +1 -0
- package/dist/restorer/engine/registry.d.ts +62 -0
- package/dist/restorer/engine/registry.js +73 -0
- package/dist/restorer/engine/registry.js.map +1 -0
- package/dist/restorer/engine/signing.d.ts +30 -0
- package/dist/restorer/engine/signing.js +39 -0
- package/dist/restorer/engine/signing.js.map +1 -0
- package/dist/restorer/engine/state.d.ts +42 -0
- package/dist/restorer/engine/state.js +87 -0
- package/dist/restorer/engine/state.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.d.ts +64 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.js +96 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.d.ts +101 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.js +710 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.d.ts +137 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.js +865 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.d.ts +74 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.js +74 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.d.ts +97 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.js +226 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.d.ts +43 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.js +230 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.d.ts +38 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.js +135 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.d.ts +8 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.js +54 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.d.ts +36 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.js +137 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.d.ts +82 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.js +6 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.js.map +1 -0
- package/dist/restorer/impls/legacy-claude/index.d.ts +45 -0
- package/dist/restorer/impls/legacy-claude/index.js +71 -0
- package/dist/restorer/impls/legacy-claude/index.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.d.ts +68 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.js +117 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.d.ts +49 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.js +91 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.d.ts +78 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.js +274 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.d.ts +23 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.js +49 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.d.ts +25 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.js +44 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.d.ts +17 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.js +43 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.d.ts +43 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.js +431 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.d.ts +21 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.js +32 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.d.ts +32 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.js +8 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.d.ts +39 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.js +98 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.d.ts +2 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.js +7 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.d.ts +4 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.js +2 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.d.ts +2 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.js +7 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.d.ts +39 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.js +186 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.d.ts +9 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.js +20 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.js +2 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.d.ts +29 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.js +94 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.d.ts +8 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.js +41 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.js +2 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.d.ts +20 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.js +66 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.d.ts +9 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.js +23 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.d.ts +3 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.js +13 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.js +93 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.d.ts +5 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.js +20 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.d.ts +33 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.js +208 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.d.ts +8 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.js +15 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.js +2 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/types.d.ts +177 -0
- package/dist/restorer/types.js +7 -0
- package/dist/restorer/types.js.map +1 -0
- package/dist/store/store.d.ts +3 -1
- package/dist/store/store.js +3 -0
- package/dist/store/store.js.map +1 -1
- package/dist/types/desired-state.d.ts +53 -0
- package/dist/types/desired-state.js +20 -0
- package/dist/types/desired-state.js.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.js +4 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/portfolio.d.ts +1000 -0
- package/dist/types/portfolio.js +168 -0
- package/dist/types/portfolio.js.map +1 -0
- package/dist/types/prediction-apy.d.ts +919 -0
- package/dist/types/prediction-apy.js +121 -0
- package/dist/types/prediction-apy.js.map +1 -0
- package/dist/types/prediction.d.ts +925 -0
- package/dist/types/prediction.js +140 -0
- package/dist/types/prediction.js.map +1 -0
- package/dist/venues/aave-v3/addresses.d.ts +6 -0
- package/dist/venues/aave-v3/addresses.js +19 -0
- package/dist/venues/aave-v3/addresses.js.map +1 -0
- package/dist/venues/aave-v3/client.d.ts +81 -0
- package/dist/venues/aave-v3/client.js +97 -0
- package/dist/venues/aave-v3/client.js.map +1 -0
- package/dist/venues/chainlink/client.d.ts +99 -0
- package/dist/venues/chainlink/client.js +130 -0
- package/dist/venues/chainlink/client.js.map +1 -0
- package/dist/venues/chainlink/feeds.d.ts +8 -0
- package/dist/venues/chainlink/feeds.js +9 -0
- package/dist/venues/chainlink/feeds.js.map +1 -0
- package/dist/venues/hyperliquid/account-value.d.ts +30 -0
- package/dist/venues/hyperliquid/account-value.js +30 -0
- package/dist/venues/hyperliquid/account-value.js.map +1 -0
- package/dist/venues/hyperliquid/client.d.ts +63 -0
- package/dist/venues/hyperliquid/client.js +135 -0
- package/dist/venues/hyperliquid/client.js.map +1 -0
- package/dist/venues/hyperliquid/grid.d.ts +36 -0
- package/dist/venues/hyperliquid/grid.js +61 -0
- package/dist/venues/hyperliquid/grid.js.map +1 -0
- package/dist/venues/hyperliquid/types.d.ts +81 -0
- package/dist/venues/hyperliquid/types.js +8 -0
- package/dist/venues/hyperliquid/types.js.map +1 -0
- package/dist/withdraw/run-withdraw-plan.js +2 -0
- package/dist/withdraw/run-withdraw-plan.js.map +1 -1
- package/docker-compose.yml +44 -0
- package/package.json +12 -1
- package/skills/jinn-operator/SKILL.md +85 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restorer engine — main RestorationEngine class.
|
|
3
|
+
*
|
|
4
|
+
* §6.3, §6.5 of spec/2026-04-17-portfolio-v0-design.md
|
|
5
|
+
*
|
|
6
|
+
* Orchestrates the state machine lifecycle for each observed intent.
|
|
7
|
+
* Transition method bodies are stubs; subsequent tasks fill them in.
|
|
8
|
+
*/
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { IntentPersistence } from './persistence.js';
|
|
11
|
+
import { IntentState, MissingEvidenceHashError } from './state.js';
|
|
12
|
+
import { executeTwoLayerClaim, releaseClaimedNotStarted, } from './claim.js';
|
|
13
|
+
import { provisionWorkingDir, provisionImplStateDir, walkArtifacts, uploadArtifacts, registerArtifacts, } from './packaging.js';
|
|
14
|
+
import { assembleAndSignManifest, } from './manifest-assembly.js';
|
|
15
|
+
import { deliverAndClaim, } from './delivery.js';
|
|
16
|
+
// ── Sentinel error ────────────────────────────────────────────────────────────
|
|
17
|
+
export class NotImplementedError extends Error {
|
|
18
|
+
transitionName;
|
|
19
|
+
constructor(transitionName) {
|
|
20
|
+
super(`[NotImplemented] ${transitionName} — fill in via subsequent task`);
|
|
21
|
+
this.name = 'NotImplementedError';
|
|
22
|
+
this.transitionName = transitionName;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// ── RestorationEngine ─────────────────────────────────────────────────────────
|
|
26
|
+
export class RestorationEngine {
|
|
27
|
+
persistence;
|
|
28
|
+
registry;
|
|
29
|
+
paths;
|
|
30
|
+
claimDeps;
|
|
31
|
+
packagingDeps;
|
|
32
|
+
manifestDeps;
|
|
33
|
+
deliveryDeps;
|
|
34
|
+
implRegistry;
|
|
35
|
+
// Transient storage for impl output between runImpl and pack transitions.
|
|
36
|
+
// Keyed by requestId; cleared after successful pack.
|
|
37
|
+
implOutputs = new Map();
|
|
38
|
+
/** Set by stop(); causes runTickLoop to exit at the next iteration. */
|
|
39
|
+
stopped = false;
|
|
40
|
+
constructor(opts) {
|
|
41
|
+
this.persistence = new IntentPersistence(opts.store.db);
|
|
42
|
+
this.registry = opts.registry;
|
|
43
|
+
this.paths = opts.paths;
|
|
44
|
+
this.claimDeps = opts.claimDeps;
|
|
45
|
+
this.packagingDeps = opts.packagingDeps;
|
|
46
|
+
this.manifestDeps = opts.manifestDeps;
|
|
47
|
+
this.deliveryDeps = opts.deliveryDeps;
|
|
48
|
+
this.implRegistry = opts.implRegistry;
|
|
49
|
+
}
|
|
50
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Called when an intent is observed from an on-chain event.
|
|
53
|
+
* Persists a DISCOVERED row. Idempotent: if the row already exists, no-op.
|
|
54
|
+
*/
|
|
55
|
+
async observe(input) {
|
|
56
|
+
const existing = this.persistence.getByRequestId(input.requestId);
|
|
57
|
+
if (!existing) {
|
|
58
|
+
this.persistence.insertDiscovered(input);
|
|
59
|
+
console.log(`[restorer-engine] observed intent ${input.requestId} kind=${input.specKind ?? 'null'}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Recover all in-flight intents from persisted state.
|
|
64
|
+
* Called at daemon startup before beginning normal event processing.
|
|
65
|
+
* Returns a per-intent report for each intent attempted.
|
|
66
|
+
*/
|
|
67
|
+
async recoverInFlight() {
|
|
68
|
+
const inflight = this.persistence.getInFlight();
|
|
69
|
+
const results = await Promise.allSettled(inflight.map((intent) => this._recoverOne(intent)));
|
|
70
|
+
const reports = results.map((result, i) => {
|
|
71
|
+
const requestId = inflight[i].requestId;
|
|
72
|
+
if (result.status === 'fulfilled') {
|
|
73
|
+
return { requestId, outcome: 'ok' };
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const error = result.reason instanceof Error
|
|
77
|
+
? result.reason.message
|
|
78
|
+
: String(result.reason);
|
|
79
|
+
return { requestId, outcome: 'failed', error };
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const okCount = reports.filter((r) => r.outcome === 'ok').length;
|
|
83
|
+
console.log(`[restorer-engine] recovery: ${okCount}/${reports.length} intents resumed`);
|
|
84
|
+
return reports;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Periodic tick: advance every in-flight intent by one transition.
|
|
88
|
+
* Called by `runTickLoop` so that intents which entered a non-event-driven
|
|
89
|
+
* state (e.g. CLAIMED waiting for windowStartTs) get re-driven without
|
|
90
|
+
* waiting for a daemon restart or a fresh marketplace event.
|
|
91
|
+
*
|
|
92
|
+
* Errors from individual intents are logged but do not stop the loop.
|
|
93
|
+
*/
|
|
94
|
+
async tick() {
|
|
95
|
+
const inflight = this.persistence.getInFlight();
|
|
96
|
+
const results = await Promise.allSettled(inflight.map((intent) => this.process(intent.requestId)));
|
|
97
|
+
for (let i = 0; i < results.length; i++) {
|
|
98
|
+
const r = results[i];
|
|
99
|
+
if (r.status === 'rejected') {
|
|
100
|
+
const requestId = inflight[i].requestId;
|
|
101
|
+
const reason = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
102
|
+
console.warn(`[restorer-engine] tick: process(${requestId}) failed: ${reason}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Drive `tick()` on a fixed interval until `stop()` is called.
|
|
108
|
+
* Errors thrown by tick() itself are logged and do not stop the loop.
|
|
109
|
+
*/
|
|
110
|
+
async runTickLoop(intervalMs) {
|
|
111
|
+
while (!this.stopped) {
|
|
112
|
+
try {
|
|
113
|
+
await this.tick();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error('[restorer-engine] tick loop error (continuing):', err instanceof Error ? err.message : err);
|
|
117
|
+
}
|
|
118
|
+
if (this.stopped)
|
|
119
|
+
break;
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Signal `runTickLoop` to exit at the next iteration. */
|
|
124
|
+
stop() {
|
|
125
|
+
this.stopped = true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Process a single intent: dispatch by current state to the appropriate
|
|
129
|
+
* transition. Drives one state transition per call.
|
|
130
|
+
*
|
|
131
|
+
* Called both by recovery and by the ongoing event-processing loop.
|
|
132
|
+
*/
|
|
133
|
+
async process(requestId) {
|
|
134
|
+
const intent = this.persistence.getByRequestId(requestId);
|
|
135
|
+
if (!intent) {
|
|
136
|
+
throw new Error(`process: intent not found: ${requestId}`);
|
|
137
|
+
}
|
|
138
|
+
switch (intent.state) {
|
|
139
|
+
case IntentState.DISCOVERED:
|
|
140
|
+
await this._runTransition(intent, () => this.claim(intent));
|
|
141
|
+
break;
|
|
142
|
+
case IntentState.CLAIMED: {
|
|
143
|
+
// Advance to WAITING — persist-before-invoke principle.
|
|
144
|
+
const oldState = intent.state;
|
|
145
|
+
this.persistence.transition(intent.requestId, IntentState.WAITING);
|
|
146
|
+
console.log(`[restorer-engine] ${requestId} ${oldState} → ${IntentState.WAITING}`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case IntentState.WAITING: {
|
|
150
|
+
const advance = this.dataDrivenAdvance(intent);
|
|
151
|
+
if (advance !== null) {
|
|
152
|
+
this.persistence.transition(intent.requestId, advance);
|
|
153
|
+
console.log(`[restorer-engine] ${requestId} ${intent.state} → ${advance}`);
|
|
154
|
+
await this._runTransition(this.persistence.getOrThrow(requestId), () => this.takePreSnapshot(this.persistence.getOrThrow(requestId)));
|
|
155
|
+
// takePreSnapshot transitions PRE_SNAPSHOT → RUNNING. Re-dispatch on
|
|
156
|
+
// the post-transition state so RUNNING fires in the same pass (jinn-mono-sae).
|
|
157
|
+
const after = this.persistence.getByRequestId(intent.requestId);
|
|
158
|
+
if (after && after.state === IntentState.RUNNING) {
|
|
159
|
+
await this.process(intent.requestId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// else: not yet time — caller is responsible for scheduling retry
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case IntentState.PRE_SNAPSHOT: {
|
|
166
|
+
const advance = this.dataDrivenAdvance(intent);
|
|
167
|
+
if (advance !== null) {
|
|
168
|
+
// Snapshot already captured (e.g. recovered from crash mid-transition)
|
|
169
|
+
this.persistence.transition(intent.requestId, advance);
|
|
170
|
+
console.log(`[restorer-engine] ${requestId} ${intent.state} → ${advance}`);
|
|
171
|
+
await this._runTransition(this.persistence.getOrThrow(requestId), () => this.runImpl(this.persistence.getOrThrow(requestId)));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await this._runTransition(intent, () => this.takePreSnapshot(intent));
|
|
175
|
+
// takePreSnapshot transitions PRE_SNAPSHOT → RUNNING internally.
|
|
176
|
+
// Re-dispatch on the post-transition state so the RUNNING case fires
|
|
177
|
+
// in the same pass (jinn-mono-sae fix). Without this, intents stall
|
|
178
|
+
// at RUNNING until the next tick/restart and runImpl never executes.
|
|
179
|
+
const after = this.persistence.getByRequestId(intent.requestId);
|
|
180
|
+
if (after && after.state === IntentState.RUNNING) {
|
|
181
|
+
await this.process(intent.requestId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case IntentState.RUNNING:
|
|
187
|
+
await this._runTransition(intent, () => this.runImpl(intent));
|
|
188
|
+
break;
|
|
189
|
+
case IntentState.POST_SNAPSHOT: {
|
|
190
|
+
const advance = this.dataDrivenAdvance(intent);
|
|
191
|
+
if (advance !== null) {
|
|
192
|
+
this.persistence.transition(intent.requestId, advance);
|
|
193
|
+
console.log(`[restorer-engine] ${requestId} ${intent.state} → ${advance}`);
|
|
194
|
+
await this._runTransition(this.persistence.getOrThrow(requestId), () => this.pack(this.persistence.getOrThrow(requestId)));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
await this._runTransition(intent, () => this.takePostSnapshot(intent));
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case IntentState.PACKAGING:
|
|
202
|
+
await this._runTransition(intent, () => this.pack(intent));
|
|
203
|
+
break;
|
|
204
|
+
case IntentState.DELIVERING:
|
|
205
|
+
await this._runTransition(intent, () => this.deliver(intent));
|
|
206
|
+
break;
|
|
207
|
+
case IntentState.COMPLETE:
|
|
208
|
+
case IntentState.FAILED:
|
|
209
|
+
// Terminal — nothing to do.
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ── Transition stubs ────────────────────────────────────────────────────────
|
|
214
|
+
// Stubs throw NotImplementedError. claim() is implemented here; others are
|
|
215
|
+
// filled in by subsequent tasks.
|
|
216
|
+
/**
|
|
217
|
+
* Two-layer claim: ClaimRegistry + Marketplace.
|
|
218
|
+
*
|
|
219
|
+
* Idempotent on resume: checks ClaimRegistry for a pre-existing claim before
|
|
220
|
+
* sending any on-chain transaction.
|
|
221
|
+
*
|
|
222
|
+
* Advances state DISCOVERED → CLAIMED on success.
|
|
223
|
+
* Marks FAILED if either layer cannot be claimed.
|
|
224
|
+
*
|
|
225
|
+
* Requires claimDeps to be injected via constructor options. Falls back to
|
|
226
|
+
* NotImplementedError if claimDeps is absent (development / test mode).
|
|
227
|
+
*/
|
|
228
|
+
async claim(intent) {
|
|
229
|
+
if (!this.claimDeps) {
|
|
230
|
+
throw new NotImplementedError('claim');
|
|
231
|
+
}
|
|
232
|
+
// ── Pre-claim impl gate ─────────────────────────────────────────────────
|
|
233
|
+
// Refuse to claim intents whose impl is either unregistered (operator has
|
|
234
|
+
// opted out via config.restorers.disabled[]) or not ready (external deps
|
|
235
|
+
// missing — e.g. HL api-wallet not approved for portfolio.v0). Marking
|
|
236
|
+
// FAILED is terminal so we don't re-attempt; another operator can still
|
|
237
|
+
// claim from the marketplace.
|
|
238
|
+
//
|
|
239
|
+
// Only fires when an implRegistry is wired in (production); tests that
|
|
240
|
+
// inject claimDeps without a registry intentionally exercise the raw
|
|
241
|
+
// claim path and are not gated.
|
|
242
|
+
if (this.implRegistry && intent.specKind) {
|
|
243
|
+
const impl = this.implRegistry.findFor({
|
|
244
|
+
kind: intent.specKind,
|
|
245
|
+
type: intent.intentType ?? 'restoration',
|
|
246
|
+
});
|
|
247
|
+
if (!impl) {
|
|
248
|
+
const reason = `no impl registered or enabled for kind '${intent.specKind}'; run \`jinn intents enable ${intent.specKind}\` to opt in`;
|
|
249
|
+
this.persistence.markFailed(intent.requestId, reason);
|
|
250
|
+
console.log(`[restorer-engine] ${intent.requestId}: skipping claim — ${reason}`);
|
|
251
|
+
throw new Error(reason);
|
|
252
|
+
}
|
|
253
|
+
if (impl.isReady) {
|
|
254
|
+
const status = await impl.isReady();
|
|
255
|
+
if (!status.ready) {
|
|
256
|
+
const reason = `impl '${impl.name}' not ready: ${status.reason ?? 'unknown'}${status.nextStep?.cli ? ` — run \`${status.nextStep.cli}\`` : ''}`;
|
|
257
|
+
this.persistence.markFailed(intent.requestId, reason);
|
|
258
|
+
console.log(`[restorer-engine] ${intent.requestId}: skipping claim — ${reason}`);
|
|
259
|
+
throw new Error(reason);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const { registryClient, marketplaceClaimer } = this.claimDeps;
|
|
264
|
+
await executeTwoLayerClaim({
|
|
265
|
+
requestId: intent.requestId,
|
|
266
|
+
windowStartTs: intent.windowStartTs,
|
|
267
|
+
}, registryClient, marketplaceClaimer);
|
|
268
|
+
// Both layers succeeded — advance state.
|
|
269
|
+
this.persistence.transition(intent.requestId, IntentState.CLAIMED);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Release ClaimRegistry claims for all CLAIMED intents whose work window has
|
|
273
|
+
* not yet started. Called on graceful engine shutdown.
|
|
274
|
+
*
|
|
275
|
+
* Returns the list of requestIds that were successfully released.
|
|
276
|
+
*/
|
|
277
|
+
async releaseClaimedNotStarted() {
|
|
278
|
+
if (!this.claimDeps) {
|
|
279
|
+
return []; // No registry client — nothing to release
|
|
280
|
+
}
|
|
281
|
+
const claimed = this.persistence.getByState(IntentState.CLAIMED);
|
|
282
|
+
const released = [];
|
|
283
|
+
for (const intent of claimed) {
|
|
284
|
+
if (Date.now() < intent.windowStartTs) {
|
|
285
|
+
try {
|
|
286
|
+
const ok = await releaseClaimedNotStarted({
|
|
287
|
+
requestId: intent.requestId,
|
|
288
|
+
windowStartTs: intent.windowStartTs,
|
|
289
|
+
}, this.claimDeps.registryClient);
|
|
290
|
+
if (ok) {
|
|
291
|
+
released.push(intent.requestId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
296
|
+
console.error(`[restorer-engine] releaseClaimedNotStarted failed for ${intent.requestId}: ${reason}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (released.length > 0) {
|
|
301
|
+
console.log(`[restorer-engine] released ${released.length} pre-window claim(s) on shutdown: ${released.join(', ')}`);
|
|
302
|
+
}
|
|
303
|
+
return released;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* PRE_SNAPSHOT transition: provision workingDir + implStateDir, write
|
|
307
|
+
* intent.json + env/ files, create sessions/ directory.
|
|
308
|
+
*
|
|
309
|
+
* Requires no external deps beyond filesystem access — always implemented.
|
|
310
|
+
* Advances state PRE_SNAPSHOT with workingDir + implStateDir patch.
|
|
311
|
+
*/
|
|
312
|
+
async takePreSnapshot(intent) {
|
|
313
|
+
const workingDir = join(this.paths.workingDirRoot, intent.requestId);
|
|
314
|
+
// Resolve the impl via registry so implStateDir matches the path runImpl uses
|
|
315
|
+
// (join(implStateDirRoot, impl.name)). Falls back to specKind then 'default'
|
|
316
|
+
// when no impl is registered — legacy path preserved for health-check intents.
|
|
317
|
+
const resolvedImpl = intent.specKind
|
|
318
|
+
? this.implRegistry?.findFor({ kind: intent.specKind, type: intent.intentType ?? 'restoration' }) ?? null
|
|
319
|
+
: null;
|
|
320
|
+
const implStateName = intent.implName ?? resolvedImpl?.name ?? intent.specKind ?? 'default';
|
|
321
|
+
const implStateDir = join(this.paths.implStateDirRoot, implStateName);
|
|
322
|
+
// Prefer the persisted full DesiredState; fall back to a stub for legacy
|
|
323
|
+
// (pre-migration) rows so the engine still works for health-check intents.
|
|
324
|
+
const desiredState = intent.desiredState ?? {
|
|
325
|
+
id: intent.requestId,
|
|
326
|
+
description: '',
|
|
327
|
+
...(intent.specKind ? { spec: { kind: intent.specKind } } : {}),
|
|
328
|
+
window: { startTs: intent.windowStartTs, endTs: intent.windowEndTs },
|
|
329
|
+
};
|
|
330
|
+
provisionWorkingDir(workingDir, desiredState);
|
|
331
|
+
provisionImplStateDir(implStateDir);
|
|
332
|
+
// takePreSnapshot transitions directly to RUNNING with the snapshot payload
|
|
333
|
+
// and workingDir/implStateDir paths set. We cannot transition
|
|
334
|
+
// PRE_SNAPSHOT → PRE_SNAPSHOT (invalid); the snapshot is immediately ready
|
|
335
|
+
// (it's just the provisioned dir context), so we advance to RUNNING in one
|
|
336
|
+
// step. The impl is responsible for capturing real market data.
|
|
337
|
+
this.persistence.transition(intent.requestId, IntentState.RUNNING, {
|
|
338
|
+
workingDir,
|
|
339
|
+
implStateDir,
|
|
340
|
+
preSnapshotCapturedAt: Date.now(),
|
|
341
|
+
preSnapshotPayload: { provisioned: true, workingDir },
|
|
342
|
+
});
|
|
343
|
+
console.log(`[restorer-engine] ${intent.requestId} PRE_SNAPSHOT → RUNNING: workingDir=${workingDir}`);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* RUNNING transition: dispatch to a RestorerImpl if implRegistry is provided.
|
|
347
|
+
*
|
|
348
|
+
* When no impl is found for the spec kind, falls back to NotImplementedError
|
|
349
|
+
* so the engine does not silently swallow the request. In tests that don't
|
|
350
|
+
* exercise the impl path, override this method.
|
|
351
|
+
*
|
|
352
|
+
* Captures impl output in `implOutputs` map for pack() to consume. Also
|
|
353
|
+
* records a minimal post-snapshot so data-driven advance can fire.
|
|
354
|
+
*/
|
|
355
|
+
async runImpl(intent) {
|
|
356
|
+
const specKind = intent.specKind ?? '';
|
|
357
|
+
const type = intent.intentType ?? 'restoration';
|
|
358
|
+
const impl = this.implRegistry?.findFor({ kind: specKind, type });
|
|
359
|
+
if (!impl) {
|
|
360
|
+
throw new NotImplementedError('runImpl');
|
|
361
|
+
}
|
|
362
|
+
const workingDir = intent.workingDir ?? join(this.paths.workingDirRoot, intent.requestId);
|
|
363
|
+
const implStateDir = intent.implStateDir ?? join(this.paths.implStateDirRoot, impl.name);
|
|
364
|
+
const windowEndTs = intent.windowEndTs;
|
|
365
|
+
const abort = new AbortController();
|
|
366
|
+
const msUntilEndTs = () => Math.max(0, windowEndTs - Date.now());
|
|
367
|
+
const endTimer = setTimeout(() => abort.abort(), msUntilEndTs());
|
|
368
|
+
try {
|
|
369
|
+
const ctx = {
|
|
370
|
+
intent: (intent.desiredState ?? {
|
|
371
|
+
id: intent.requestId,
|
|
372
|
+
description: '',
|
|
373
|
+
...(intent.specKind ? { spec: { kind: intent.specKind } } : {}),
|
|
374
|
+
window: { startTs: intent.windowStartTs, endTs: intent.windowEndTs },
|
|
375
|
+
}),
|
|
376
|
+
implStateDir,
|
|
377
|
+
workingDir,
|
|
378
|
+
log: (event) => {
|
|
379
|
+
console.log(`[restorer-impl:${impl.name}] [${event.level}] ${event.msg}`, event.data ?? '');
|
|
380
|
+
},
|
|
381
|
+
abort: abort.signal,
|
|
382
|
+
msUntilEndTs,
|
|
383
|
+
};
|
|
384
|
+
let output;
|
|
385
|
+
try {
|
|
386
|
+
output = await impl.run(ctx);
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
if (impl.name === 'legacy-claude' && isClaudeUnavailableError(err)) {
|
|
390
|
+
const skippedAt = Date.now();
|
|
391
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
392
|
+
console.warn(`[restorer-engine] ${intent.requestId}: legacy-claude unavailable, skipping intent (${detail})`);
|
|
393
|
+
output = {
|
|
394
|
+
venueRef: { name: 'legacy' },
|
|
395
|
+
gating: {
|
|
396
|
+
skipped: true,
|
|
397
|
+
reason: 'claude_unavailable',
|
|
398
|
+
skippedAt: String(skippedAt),
|
|
399
|
+
},
|
|
400
|
+
informational: {
|
|
401
|
+
status: 'skipped',
|
|
402
|
+
detail,
|
|
403
|
+
},
|
|
404
|
+
artifacts: [],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.implOutputs.set(intent.requestId, output);
|
|
412
|
+
// Persist impl output BEFORE the state transition so that a crash after
|
|
413
|
+
// the transition (RUNNING → POST_SNAPSHOT) but before pack() runs will
|
|
414
|
+
// find the serialised output in the DB on restart. pack() will hydrate the
|
|
415
|
+
// in-memory map from implOutputsJson if the map entry is absent (#6).
|
|
416
|
+
// Capture post-snapshot from impl output so data-driven advance fires
|
|
417
|
+
this.persistence.transition(intent.requestId, IntentState.POST_SNAPSHOT, {
|
|
418
|
+
postSnapshotCapturedAt: Date.now(),
|
|
419
|
+
postSnapshotPayload: output.postSnapshot ?? { capturedAt: Date.now(), hlTime: 0, payload: null },
|
|
420
|
+
fillsPayload: output.fills ?? [],
|
|
421
|
+
gatingClaim: output.gating,
|
|
422
|
+
informationalClaim: output.informational ?? null,
|
|
423
|
+
implOutputsJson: JSON.stringify(output),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
clearTimeout(endTimer);
|
|
428
|
+
}
|
|
429
|
+
console.log(`[restorer-engine] ${intent.requestId} RUNNING → POST_SNAPSHOT via impl=${impl.name}`);
|
|
430
|
+
}
|
|
431
|
+
async takePostSnapshot(_intent) {
|
|
432
|
+
throw new NotImplementedError('takePostSnapshot');
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* PACKAGING transition: walk workingDir, upload artifacts, assemble + sign
|
|
436
|
+
* manifest, upload manifest, persist manifest CID + artifact CIDs.
|
|
437
|
+
*
|
|
438
|
+
* Requires packagingDeps + manifestDeps. When absent, falls back to
|
|
439
|
+
* NotImplementedError.
|
|
440
|
+
*/
|
|
441
|
+
async pack(intent) {
|
|
442
|
+
// Hydrate implOutput from DB if the in-memory map was lost (e.g. process restart
|
|
443
|
+
// after RUNNING → POST_SNAPSHOT but before pack() completed). This must run
|
|
444
|
+
// BEFORE the packagingDeps guard so subclass overrides that call super.pack()
|
|
445
|
+
// can still benefit from hydration even when packagingDeps is absent (#6).
|
|
446
|
+
if (!this.implOutputs.has(intent.requestId) && intent.implOutputsJson != null) {
|
|
447
|
+
try {
|
|
448
|
+
const recovered = JSON.parse(intent.implOutputsJson);
|
|
449
|
+
this.implOutputs.set(intent.requestId, recovered);
|
|
450
|
+
console.log(`[restorer-engine] ${intent.requestId}: hydrated implOutputs from DB (crash recovery)`);
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
console.warn(`[restorer-engine] ${intent.requestId}: failed to hydrate implOutputsJson: ${err instanceof Error ? err.message : err}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!this.packagingDeps || !this.manifestDeps) {
|
|
457
|
+
throw new NotImplementedError('pack');
|
|
458
|
+
}
|
|
459
|
+
const workingDir = intent.workingDir ?? join(this.paths.workingDirRoot, intent.requestId);
|
|
460
|
+
const implOutput = this.implOutputs.get(intent.requestId);
|
|
461
|
+
const implArtifacts = implOutput?.artifacts ?? [];
|
|
462
|
+
// 1. Walk + upload artifacts (NO registration yet — manifest CID not known)
|
|
463
|
+
const rawArtifacts = await walkArtifacts(workingDir, implArtifacts);
|
|
464
|
+
const uploadedArtifacts = await uploadArtifacts(rawArtifacts, this.packagingDeps);
|
|
465
|
+
// Map to Artifact shape (strip localPath)
|
|
466
|
+
const artifacts = uploadedArtifacts.map(({ localPath: _localPath, ...art }) => art);
|
|
467
|
+
// 2. Derive agentEoa from private key
|
|
468
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
469
|
+
const account = privateKeyToAccount(this.manifestDeps.agentEoaPrivateKey);
|
|
470
|
+
const agentEoa = account.address;
|
|
471
|
+
// Safe multisig address — sourced from manifestDeps (preferred) or deliveryDeps.
|
|
472
|
+
// Hard throw if absent: falling back to agentEoa would produce a
|
|
473
|
+
// protocol-invalid manifest (safeAddress MUST differ from agentEoa, §5.1).
|
|
474
|
+
const safeAddress = this.manifestDeps.safeAddress ?? this.deliveryDeps?.safeAddress;
|
|
475
|
+
if (!safeAddress) {
|
|
476
|
+
throw new Error('pack: safeAddress not configured in manifestDeps or deliveryDeps');
|
|
477
|
+
}
|
|
478
|
+
// 3. Assemble + sign manifest
|
|
479
|
+
const provenance = {
|
|
480
|
+
intentCid: intent.intentCid,
|
|
481
|
+
onchainCreationTx: intent.onchainCreationTx,
|
|
482
|
+
onchainCreationBlock: intent.onchainCreationBlock,
|
|
483
|
+
requestId: intent.requestId,
|
|
484
|
+
safeAddress,
|
|
485
|
+
agentEoa,
|
|
486
|
+
windowStartTs: intent.windowStartTs,
|
|
487
|
+
windowEndTs: intent.windowEndTs,
|
|
488
|
+
};
|
|
489
|
+
const preSnapshotPayload = intent.preSnapshotPayload;
|
|
490
|
+
const postSnapshotPayload = intent.postSnapshotPayload;
|
|
491
|
+
const manifestImplOutput = {
|
|
492
|
+
preSnapshot: {
|
|
493
|
+
capturedAt: intent.preSnapshotCapturedAt ?? Date.now(),
|
|
494
|
+
hlTime: preSnapshotPayload?.hlTime ?? 0,
|
|
495
|
+
// Double-fallback: first tries the structured .payload field (normal shape),
|
|
496
|
+
// then falls back to the whole payload object (handles takePreSnapshot's
|
|
497
|
+
// synthetic shape where the snapshot IS the top-level object, not nested).
|
|
498
|
+
payload: preSnapshotPayload?.payload ?? preSnapshotPayload ?? {},
|
|
499
|
+
},
|
|
500
|
+
postSnapshot: {
|
|
501
|
+
capturedAt: intent.postSnapshotCapturedAt ?? Date.now(),
|
|
502
|
+
hlTime: postSnapshotPayload?.hlTime ?? 0,
|
|
503
|
+
// Same double-fallback as above.
|
|
504
|
+
payload: postSnapshotPayload?.payload ?? postSnapshotPayload ?? {},
|
|
505
|
+
},
|
|
506
|
+
fills: intent.fillsPayload ?? [],
|
|
507
|
+
gating: intent.gatingClaim ?? {},
|
|
508
|
+
informational: intent.informationalClaim ?? undefined,
|
|
509
|
+
rationale: implOutput?.rationale ?? undefined,
|
|
510
|
+
};
|
|
511
|
+
// 4. Persist generatedAt once (first pack); reuse on retry for CID determinism.
|
|
512
|
+
const generatedAt = intent.manifestGeneratedAt ?? Date.now();
|
|
513
|
+
if (!intent.manifestGeneratedAt) {
|
|
514
|
+
// Persist before assembling so that a crash after assembly but before
|
|
515
|
+
// transition still gets the same generatedAt on the next attempt.
|
|
516
|
+
this.persistence.setManifestGeneratedAt(intent.requestId, generatedAt);
|
|
517
|
+
}
|
|
518
|
+
const manifestOpts = { generatedAt };
|
|
519
|
+
// 5. Sign + upload manifest → manifest CID now known
|
|
520
|
+
const { manifest: _manifest, manifestCid, signatureHash } = await assembleAndSignManifest(provenance, manifestImplOutput, artifacts, this.manifestDeps, manifestOpts);
|
|
521
|
+
// 6. Register artifacts with back-pointer to parent manifest CID (spec §6.1)
|
|
522
|
+
registerArtifacts(uploadedArtifacts, manifestCid, this.packagingDeps);
|
|
523
|
+
// 7. Build artifact CID map for persistence
|
|
524
|
+
const artifactCids = {};
|
|
525
|
+
for (const art of uploadedArtifacts) {
|
|
526
|
+
artifactCids[art.localPath] = art.cid;
|
|
527
|
+
}
|
|
528
|
+
// 8. Persist DELIVERING with manifest CID + artifact CIDs + evidence hash.
|
|
529
|
+
// evidenceHash gets its own dedicated column (not stashed in informationalClaim).
|
|
530
|
+
this.persistence.transition(intent.requestId, IntentState.DELIVERING, {
|
|
531
|
+
manifestCid,
|
|
532
|
+
artifactCids,
|
|
533
|
+
evidenceHash: signatureHash,
|
|
534
|
+
});
|
|
535
|
+
console.log(`[restorer-engine] ${intent.requestId} PACKAGING → DELIVERING manifestCid=${manifestCid}`);
|
|
536
|
+
// Clean up impl output (no longer needed)
|
|
537
|
+
this.implOutputs.delete(intent.requestId);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* DELIVERING transition: call mech.deliverToMarketplace + JinnRouter.claimDelivery.
|
|
541
|
+
*
|
|
542
|
+
* Requires deliveryDeps. When absent, falls back to NotImplementedError.
|
|
543
|
+
*
|
|
544
|
+
* Crash-recovery safe: if `intent.deliveryTxHash` is already set (persisted
|
|
545
|
+
* after a previous deliverToMarketplace call that completed before the process
|
|
546
|
+
* crashed), we skip the deliver step and go straight to claimDelivery.
|
|
547
|
+
*/
|
|
548
|
+
async deliver(intent) {
|
|
549
|
+
if (!this.deliveryDeps) {
|
|
550
|
+
throw new NotImplementedError('deliver');
|
|
551
|
+
}
|
|
552
|
+
const manifestCid = intent.manifestCid;
|
|
553
|
+
if (!manifestCid) {
|
|
554
|
+
throw new Error(`deliver: manifestCid missing for ${intent.requestId}`);
|
|
555
|
+
}
|
|
556
|
+
// Guard: v2 claimDelivery requires an evidenceHash — a zero fallback would
|
|
557
|
+
// silently brick staking rewards, so we fail loudly instead.
|
|
558
|
+
const evidenceHash = intent.evidenceHash;
|
|
559
|
+
if (!evidenceHash && this.deliveryDeps.claimDeliveryVariant === 'v2') {
|
|
560
|
+
throw new MissingEvidenceHashError(intent.requestId);
|
|
561
|
+
}
|
|
562
|
+
// Capture locals for use in the onDeliveryTxLanded closure.
|
|
563
|
+
const requestId = intent.requestId;
|
|
564
|
+
const persistence = this.persistence;
|
|
565
|
+
const { deliveryTxHash, claimTxHash } = await deliverAndClaim(requestId, manifestCid, evidenceHash, this.deliveryDeps,
|
|
566
|
+
// Recovery: pass existing deliveryTxHash so deliverToMarketplace is skipped.
|
|
567
|
+
intent.deliveryTxHash ?? undefined,
|
|
568
|
+
// Persist deliveryTxHash before claimDelivery so recovery can resume from here.
|
|
569
|
+
async (txHash) => {
|
|
570
|
+
persistence.setDeliveryTxHash(requestId, txHash);
|
|
571
|
+
});
|
|
572
|
+
this.persistence.transition(requestId, IntentState.COMPLETE, {
|
|
573
|
+
deliveryTxHash,
|
|
574
|
+
});
|
|
575
|
+
console.log(`[restorer-engine] ${requestId} DELIVERING → COMPLETE deliveryTx=${deliveryTxHash} claimTx=${claimTxHash}`);
|
|
576
|
+
}
|
|
577
|
+
// ── Internal helpers ────────────────────────────────────────────────────────
|
|
578
|
+
/**
|
|
579
|
+
* Returns the next state if the current state can be advanced purely from
|
|
580
|
+
* persisted data (no external work needed), or null if external work is required.
|
|
581
|
+
*
|
|
582
|
+
* Used for crash recovery and for collapsing transitions in process()
|
|
583
|
+
* when a previous run already produced the data.
|
|
584
|
+
*/
|
|
585
|
+
dataDrivenAdvance(intent) {
|
|
586
|
+
switch (intent.state) {
|
|
587
|
+
case IntentState.WAITING:
|
|
588
|
+
return Date.now() >= intent.windowStartTs ? IntentState.PRE_SNAPSHOT : null;
|
|
589
|
+
case IntentState.PRE_SNAPSHOT:
|
|
590
|
+
return intent.preSnapshotPayload != null ? IntentState.RUNNING : null;
|
|
591
|
+
case IntentState.POST_SNAPSHOT:
|
|
592
|
+
return intent.postSnapshotPayload != null ? IntentState.PACKAGING : null;
|
|
593
|
+
default:
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Wraps a transition method call with error handling: if the transition
|
|
599
|
+
* throws, the intent is marked FAILED with the error message.
|
|
600
|
+
*/
|
|
601
|
+
async _runTransition(intent, fn) {
|
|
602
|
+
const oldState = intent.state;
|
|
603
|
+
try {
|
|
604
|
+
await fn();
|
|
605
|
+
const updated = this.persistence.getByRequestId(intent.requestId);
|
|
606
|
+
if (updated && updated.state !== oldState) {
|
|
607
|
+
console.log(`[restorer-engine] ${intent.requestId} ${oldState} → ${updated.state}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
612
|
+
this.persistence.markFailed(intent.requestId, reason);
|
|
613
|
+
throw err;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Recovery handler for a single in-flight intent.
|
|
618
|
+
* Dispatches by state per §6.5.
|
|
619
|
+
*/
|
|
620
|
+
async _recoverOne(intent) {
|
|
621
|
+
try {
|
|
622
|
+
await this._recoverDispatch(intent);
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
// If recovery itself throws (e.g. NotImplementedError stub), mark failed.
|
|
626
|
+
// NotImplementedError is expected during development; don't swallow it in prod.
|
|
627
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
628
|
+
// Only mark failed if the intent is still in the same non-terminal state
|
|
629
|
+
// (another concurrent recovery pass might have already advanced it).
|
|
630
|
+
const current = this.persistence.getByRequestId(intent.requestId);
|
|
631
|
+
if (current && current.state === intent.state) {
|
|
632
|
+
this.persistence.markFailed(intent.requestId, `recovery: ${reason}`);
|
|
633
|
+
console.error(`[restorer-engine] resume failed for ${intent.requestId}: ${reason}`);
|
|
634
|
+
}
|
|
635
|
+
throw err;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Per-state recovery dispatch per §6.5.
|
|
640
|
+
*/
|
|
641
|
+
async _recoverDispatch(intent) {
|
|
642
|
+
switch (intent.state) {
|
|
643
|
+
case IntentState.DISCOVERED:
|
|
644
|
+
// Ready to claim — delegate to claim flow (subsequent task).
|
|
645
|
+
// Stub: leaves state unchanged; logs intent is ready.
|
|
646
|
+
await this.claim(intent);
|
|
647
|
+
break;
|
|
648
|
+
case IntentState.CLAIMED:
|
|
649
|
+
// Advance to WAITING — no side effect needed.
|
|
650
|
+
this.persistence.transition(intent.requestId, IntentState.WAITING);
|
|
651
|
+
await this._recoverDispatch(this.persistence.getOrThrow(intent.requestId));
|
|
652
|
+
break;
|
|
653
|
+
case IntentState.WAITING: {
|
|
654
|
+
const advance = this.dataDrivenAdvance(intent);
|
|
655
|
+
if (advance !== null) {
|
|
656
|
+
// Window has started — advance immediately.
|
|
657
|
+
this.persistence.transition(intent.requestId, advance);
|
|
658
|
+
await this._recoverDispatch(this.persistence.getOrThrow(intent.requestId));
|
|
659
|
+
}
|
|
660
|
+
// else: schedule a timer for startTs — caller handles scheduling.
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
case IntentState.PRE_SNAPSHOT: {
|
|
664
|
+
const advance = this.dataDrivenAdvance(intent);
|
|
665
|
+
if (advance !== null) {
|
|
666
|
+
// Snapshot already in DB — advance to RUNNING.
|
|
667
|
+
this.persistence.transition(intent.requestId, advance);
|
|
668
|
+
await this._recoverDispatch(this.persistence.getOrThrow(intent.requestId));
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
// Need to (re-)fetch snapshot.
|
|
672
|
+
await this.takePreSnapshot(intent);
|
|
673
|
+
// takePreSnapshot transitions PRE_SNAPSHOT → RUNNING. Re-dispatch
|
|
674
|
+
// against the post-transition state so runImpl actually fires for
|
|
675
|
+
// intents that were persisted at CLAIMED/WAITING/PRE_SNAPSHOT
|
|
676
|
+
// before a restart (otherwise recovery stops at RUNNING-but-not-run).
|
|
677
|
+
const after = this.persistence.getByRequestId(intent.requestId);
|
|
678
|
+
if (after && after.state !== intent.state && after.state !== IntentState.FAILED) {
|
|
679
|
+
await this._recoverDispatch(after);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case IntentState.RUNNING:
|
|
685
|
+
// Re-spawn impl with workingDir + implStateDir intact.
|
|
686
|
+
await this.runImpl(intent);
|
|
687
|
+
break;
|
|
688
|
+
case IntentState.POST_SNAPSHOT: {
|
|
689
|
+
const advance = this.dataDrivenAdvance(intent);
|
|
690
|
+
if (advance !== null) {
|
|
691
|
+
// Snapshot already in DB — advance to PACKAGING.
|
|
692
|
+
this.persistence.transition(intent.requestId, advance);
|
|
693
|
+
await this._recoverDispatch(this.persistence.getOrThrow(intent.requestId));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
await this.takePostSnapshot(intent);
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case IntentState.PACKAGING:
|
|
701
|
+
// Re-walk workingDir + RestorationOutput; re-upload missing CIDs.
|
|
702
|
+
await this.pack(intent);
|
|
703
|
+
break;
|
|
704
|
+
case IntentState.DELIVERING:
|
|
705
|
+
// Chain query — if already delivered → COMPLETE; else retry.
|
|
706
|
+
await this.deliver(intent);
|
|
707
|
+
break;
|
|
708
|
+
case IntentState.COMPLETE:
|
|
709
|
+
case IntentState.FAILED:
|
|
710
|
+
// Terminal — nothing to recover.
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Narrow heuristic: only match phrases that unambiguously indicate the Claude
|
|
716
|
+
// CLI is unavailable (not logged in, quota exhausted, API key rejected). This
|
|
717
|
+
// is intentionally narrow so that transient failures surfaced *through* a
|
|
718
|
+
// Claude-orchestrated tool call (network errors, RPC timeouts, upstream 5xx
|
|
719
|
+
// that happens to carry a generic "rate limit exceeded") still propagate as
|
|
720
|
+
// hard failures. Callers additionally gate this behind
|
|
721
|
+
// `impl.name === 'legacy-claude'`.
|
|
722
|
+
function isClaudeUnavailableError(err) {
|
|
723
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
724
|
+
// Phrases that unambiguously point at the Claude CLI itself.
|
|
725
|
+
const claudeSpecificPhrases = [
|
|
726
|
+
'claude auth',
|
|
727
|
+
'claude not authenticated',
|
|
728
|
+
'claude cli not found',
|
|
729
|
+
'claude: command not found',
|
|
730
|
+
'claude quota',
|
|
731
|
+
'claude rate limit',
|
|
732
|
+
'claude credit',
|
|
733
|
+
'anthropic api key',
|
|
734
|
+
];
|
|
735
|
+
if (claudeSpecificPhrases.some((p) => msg.includes(p)))
|
|
736
|
+
return true;
|
|
737
|
+
// Secondary: message must mention claude AND a clearly-availability phrase.
|
|
738
|
+
if (msg.includes('claude')) {
|
|
739
|
+
const availabilityPhrases = [
|
|
740
|
+
'not logged in',
|
|
741
|
+
'please login',
|
|
742
|
+
'please log in',
|
|
743
|
+
'quota exhausted',
|
|
744
|
+
'quota exceeded',
|
|
745
|
+
'credit limit',
|
|
746
|
+
'invalid api key',
|
|
747
|
+
'api key not found',
|
|
748
|
+
];
|
|
749
|
+
return availabilityPhrases.some((p) => msg.includes(p));
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
//# sourceMappingURL=engine.js.map
|