@stelis/say-ur-intent 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -39
- package/dist/adapters/adapterLifecycleValidators.js +7 -0
- package/dist/adapters/adapterPromptSurfaces.js +71 -0
- package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +175 -0
- package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
- package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
- package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
- package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +269 -0
- package/dist/adapters/flowx/flowxSwapHumanReviewProducer.js +176 -0
- package/dist/adapters/flowx/flowxSwapIntent.js +79 -0
- package/dist/adapters/flowx/flowxSwapQuotePolicy.js +104 -0
- package/dist/adapters/flowx/flowxSwapReviewEvidence.js +468 -0
- package/dist/adapters/flowx/flowxSwapReviewLifecycle.js +85 -0
- package/dist/adapters/flowx/flowxSwapTransactionMaterialProducer.js +362 -0
- package/dist/adapters/intentPlanFactories.js +59 -0
- package/dist/adapters/reviewAdapters.js +81 -0
- package/dist/core/action/adapterLifecycleValidation.js +12 -0
- package/dist/core/action/forbiddenFields.js +43 -0
- package/dist/core/action/humanReadableReviewEvidence.js +203 -0
- package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
- package/dist/core/action/ptbVisualizationProducer.js +66 -0
- package/dist/core/action/reviewCheckResults.js +6 -0
- package/dist/core/action/reviewStateValidation.js +11 -0
- package/dist/core/action/reviewTimeSimulationEvidence.js +471 -0
- package/dist/core/action/schemas.js +529 -0
- package/dist/core/action/signableAdapterContract.js +993 -0
- package/dist/core/action/swapHumanReadableReviewProjection.js +124 -0
- package/dist/core/action/swapQuotePolicyEvidence.js +278 -0
- package/dist/core/action/transactionObjectOwnershipEvidence.js +247 -0
- package/dist/core/action/transactionObjectOwnershipProducer.js +329 -0
- package/dist/core/action/types.js +35 -0
- package/dist/core/action/walletReviewContractAssembler.js +282 -0
- package/dist/core/activity/activityStore.js +15 -0
- package/dist/core/activity/localDataService.js +258 -0
- package/dist/core/activity/localDataTypes.js +11 -0
- package/dist/core/activity/localDataValidation.js +396 -0
- package/dist/core/activity/schemaVersion.js +1 -0
- package/dist/core/activity/sqliteActivityStore.js +820 -0
- package/dist/core/activity/sqliteActivityStoreRows.js +430 -0
- package/dist/core/activity/sqliteActivityStoreSchema.js +258 -0
- package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
- package/dist/core/activity/suiFunctionTarget.js +43 -0
- package/dist/core/activity/transactionActivityAccountEffects.js +189 -0
- package/dist/core/activity/transactionActivityAnalysis.js +295 -0
- package/dist/core/activity/transactionActivityClassifier.js +306 -0
- package/dist/core/activity/transactionActivityDetails.js +229 -0
- package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
- package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
- package/dist/core/activity/transactionActivityService.js +379 -0
- package/dist/core/activity/transactionActivityTypes.js +18 -0
- package/dist/core/eventlog/sink.js +35 -0
- package/dist/core/evidence/settlementFamilies.js +87 -0
- package/dist/core/evidence/userAnswerUse.js +1 -0
- package/dist/core/numeric/rawU64.js +63 -0
- package/dist/core/preferences/preferencesStore.js +26 -0
- package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
- package/dist/core/proposal/externalProposalReview.js +347 -0
- package/dist/core/proposal/schemas.js +208 -0
- package/dist/core/proposal/types.js +35 -0
- package/dist/core/read/amounts.js +14 -0
- package/dist/core/read/coinMetadata.js +60 -0
- package/dist/core/read/deepbookRawQuoteClient.js +86 -0
- package/dist/core/read/deepbookReadHelpers.js +265 -0
- package/dist/core/read/deepbookRegistry.js +133 -0
- package/dist/core/read/flowxQuoteClient.js +117 -0
- package/dist/core/read/flowxReadHelpers.js +145 -0
- package/dist/core/read/flowxRegistry.js +174 -0
- package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
- package/dist/core/read/readResponseGuidance.js +451 -0
- package/dist/core/read/readService.js +1164 -0
- package/dist/core/read/readServiceTypes.js +59 -0
- package/dist/core/read/settlementParityFormatting.js +82 -0
- package/dist/core/read/walletReadHelpers.js +99 -0
- package/dist/core/review/reviewChecks.js +54 -0
- package/dist/core/review/reviewComputation.js +38 -0
- package/dist/core/review/reviewComputationResult.js +87 -0
- package/dist/core/session/localSession.js +31 -0
- package/dist/core/session/privateReviewArtifacts.js +73 -0
- package/dist/core/session/sessionErrors.js +9 -0
- package/dist/core/session/sessionStore.js +821 -0
- package/dist/core/session/settingsSession.js +1 -0
- package/dist/core/session/settingsSessions.js +43 -0
- package/dist/core/session/status.js +86 -0
- package/dist/core/session/transactionMaterialStore.js +205 -0
- package/dist/core/session/wait.js +102 -0
- package/dist/core/session/walletIdentity.js +103 -0
- package/dist/core/session/walletIdentitySessions.js +189 -0
- package/dist/core/suiAddress.js +18 -0
- package/dist/core/suiEndpoint.js +72 -0
- package/dist/mcp/activeAccountResponse.js +24 -0
- package/dist/mcp/prompts.js +146 -0
- package/dist/mcp/registerTool.js +19 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/responseGuidance.js +381 -0
- package/dist/mcp/result.js +17 -0
- package/dist/mcp/schemas.js +8 -0
- package/dist/mcp/server.js +30 -0
- package/dist/mcp/serverInfo.js +123 -0
- package/dist/mcp/toolErrors.js +105 -0
- package/dist/mcp/toolNames.js +50 -0
- package/dist/mcp/tools/account/index.js +44 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +120 -0
- package/dist/mcp/tools/read/commonSchemas.js +43 -0
- package/dist/mcp/tools/read/deepbookReadTools.js +453 -0
- package/dist/mcp/tools/read/flowxReadTools.js +135 -0
- package/dist/mcp/tools/read/index.js +16 -0
- package/dist/mcp/tools/read/readToolHelpers.js +68 -0
- package/dist/mcp/tools/read/reviewActivityTools.js +176 -0
- package/dist/mcp/tools/read/serverStatusTools.js +103 -0
- package/dist/mcp/tools/read/transactionActivityOutput.js +300 -0
- package/dist/mcp/tools/read/transactionActivityTools.js +544 -0
- package/dist/mcp/tools/read/walletReadTools.js +733 -0
- package/dist/mcp/tools/session/executionResultTools.js +92 -0
- package/dist/mcp/tools/session/index.js +8 -0
- package/dist/mcp/tools/session/shared.js +79 -0
- package/dist/mcp/tools/session/statusTools.js +134 -0
- package/dist/mcp/tools/session/walletIdentityTools.js +119 -0
- package/dist/mcp/tools/settings/index.js +64 -0
- package/dist/review-app/analysis.css +1 -0
- package/dist/review-app/analysis.js +1 -0
- package/dist/review-app/arc-BjIacwQm.js +1 -0
- package/dist/review-app/architecture-U656AL7Q-aSB9x1OK.js +1 -0
- package/dist/review-app/architectureDiagram-VXUJARFQ-C5W6re2I.js +36 -0
- package/dist/review-app/array-BmXUUrU6.js +1 -0
- package/dist/review-app/blockDiagram-VD42YOAC-20MLNcUm.js +122 -0
- package/dist/review-app/c4Diagram-YG6GDRKO-BZXRrcck.js +10 -0
- package/dist/review-app/channel-lk2p_CUu.js +1 -0
- package/dist/review-app/chunk-4BX2VUAB-BPITOdjX.js +1 -0
- package/dist/review-app/chunk-55IACEB6-Dz-pyw5k.js +1 -0
- package/dist/review-app/chunk-76Q3JFCE-cK_X1P_l.js +1 -0
- package/dist/review-app/chunk-ABZYJK2D-Dt4W53JI.js +81 -0
- package/dist/review-app/chunk-ATLVNIR6-fZHLXURb.js +1 -0
- package/dist/review-app/chunk-B4BG7PRW-BbgcjusC.js +165 -0
- package/dist/review-app/chunk-BJD4TVEz.js +1 -0
- package/dist/review-app/chunk-CVBHYZKI-CViawAKX.js +1 -0
- package/dist/review-app/chunk-DI55MBZ5-C5aoul-d.js +220 -0
- package/dist/review-app/chunk-FMBD7UC4-Chxmw62A.js +15 -0
- package/dist/review-app/chunk-FPAJGGOC-DDHjQ09H.js +80 -0
- package/dist/review-app/chunk-FWNWRKHM-CVVQUptk.js +1 -0
- package/dist/review-app/chunk-HN2XXSSU-yzNpjaSZ.js +1 -0
- package/dist/review-app/chunk-JA3XYJ7Z-C5ZJdU01.js +70 -0
- package/dist/review-app/chunk-JZLCHNYA-BBST4Cnk.js +54 -0
- package/dist/review-app/chunk-LBM3YZW2-CdwAPuHr.js +1 -0
- package/dist/review-app/chunk-LHMN2FUI-BtB5uDcp.js +1 -0
- package/dist/review-app/chunk-O7ZBX7Z2-pxdK4Sa3.js +1 -0
- package/dist/review-app/chunk-QN33PNHL-CbVv3uGK.js +1 -0
- package/dist/review-app/chunk-QXUST7PY-DKM2-t2c.js +7 -0
- package/dist/review-app/chunk-QZHKN3VN-C5ni2pN_.js +1 -0
- package/dist/review-app/chunk-S3R3BYOJ-BWvOhDs0.js +2 -0
- package/dist/review-app/chunk-S6J4BHB3-D9Fk0YeD.js +1 -0
- package/dist/review-app/chunk-T53DSG4Q-C1qEyzyV.js +1 -0
- package/dist/review-app/chunk-TZMSLE5B-B--7eU69.js +1 -0
- package/dist/review-app/classDiagram-2ON5EDUG-DlL1m2bp.js +1 -0
- package/dist/review-app/classDiagram-v2-WZHVMYZB-FXRskT1j.js +1 -0
- package/dist/review-app/clone-BZZb7gpZ.js +1 -0
- package/dist/review-app/cose-bilkent-S5V4N54A-CRIb8XEO.js +1 -0
- package/dist/review-app/cytoscape.esm-C7jYqDP5.js +321 -0
- package/dist/review-app/dagre-6UL2VRFP-FNCAXbdE.js +4 -0
- package/dist/review-app/dagre-Be46QtUd.js +1 -0
- package/dist/review-app/defaultLocale-BaWNtAUL.js +1 -0
- package/dist/review-app/diagram-PSM6KHXK-ylLWjiNM.js +24 -0
- package/dist/review-app/diagram-QEK2KX5R-BCDcESxs.js +43 -0
- package/dist/review-app/diagram-S2PKOQOG-Vdrc-vrO.js +24 -0
- package/dist/review-app/dist-WPc74x_f.js +1 -0
- package/dist/review-app/erDiagram-Q2GNP2WA-E5ZsUbDF.js +60 -0
- package/dist/review-app/flatten-DHf9IeNI.js +1 -0
- package/dist/review-app/flowDiagram-NV44I4VS-DBSQuj6x.js +162 -0
- package/dist/review-app/ganttDiagram-LVOFAZNH-CKUOsqwl.js +267 -0
- package/dist/review-app/gitGraph-F6HP7TQM-DsAD6qK1.js +1 -0
- package/dist/review-app/gitGraphDiagram-NY62KEGX-BCeIMWdl.js +65 -0
- package/dist/review-app/graphlib-CiX5CXxR.js +1 -0
- package/dist/review-app/http-DMvwuuFk.js +1 -0
- package/dist/review-app/identity-DY8PXc6t.js +1 -0
- package/dist/review-app/info-NVLQJR56-Dlx1nZic.js +1 -0
- package/dist/review-app/infoDiagram-F6ZHWCRC-CAuANIrz.js +2 -0
- package/dist/review-app/init-BvqephKz.js +1 -0
- package/dist/review-app/journeyDiagram-XKPGCS4Q-C-Z9phnx.js +139 -0
- package/dist/review-app/kanban-definition-3W4ZIXB7-DufgZABq.js +89 -0
- package/dist/review-app/katex-B-Z-NXXN.js +257 -0
- package/dist/review-app/line-DiIv3Jgw.js +1 -0
- package/dist/review-app/linear-Cv-UPvo1.js +1 -0
- package/dist/review-app/math-kmyYrkHL.js +1 -0
- package/dist/review-app/mermaid-parser.core-DkwUYTPl.js +4 -0
- package/dist/review-app/mindmap-definition-VGOIOE7T-TM_CqdmV.js +68 -0
- package/dist/review-app/ordinal-BliTlkoG.js +1 -0
- package/dist/review-app/packet-BFZMPI3H-DqbnU92v.js +1 -0
- package/dist/review-app/path-AEo9W6mQ.js +1 -0
- package/dist/review-app/pie-7BOR55EZ-LJzaLkgr.js +1 -0
- package/dist/review-app/pieDiagram-ADFJNKIX-BAs8OfRS.js +30 -0
- package/dist/review-app/quadrantDiagram-AYHSOK5B-CyUDZP5S.js +7 -0
- package/dist/review-app/radar-NHE76QYJ-DBpHc8_Y.js +1 -0
- package/dist/review-app/reduce-B-HuPpdd.js +1 -0
- package/dist/review-app/requirementDiagram-UZGBJVZJ-BEHix78P.js +64 -0
- package/dist/review-app/review.css +1 -0
- package/dist/review-app/review.js +43 -0
- package/dist/review-app/sankeyDiagram-TZEHDZUN-B2bKbmsm.js +10 -0
- package/dist/review-app/sequenceDiagram-WL72ISMW-DVLOORFJ.js +145 -0
- package/dist/review-app/settings.css +1 -0
- package/dist/review-app/settings.js +1 -0
- package/dist/review-app/src-Buml7cM5.js +1 -0
- package/dist/review-app/stateDiagram-FKZM4ZOC-sFGGp2kV.js +1 -0
- package/dist/review-app/stateDiagram-v2-4FDKWEC3-BHfCF4dX.js +1 -0
- package/dist/review-app/timeline-definition-IT6M3QCI-BESnBijC.js +61 -0
- package/dist/review-app/treemap-KMMF4GRG-wnVLBDeQ.js +1 -0
- package/dist/review-app/walletStatus-CcojOdGy.js +7 -0
- package/dist/review-app/xychartDiagram-PRI3JC2R-BGWVfCx4.js +7 -0
- package/dist/review-server/assets.js +48 -0
- package/dist/review-server/html.js +66 -0
- package/dist/review-server/http.js +47 -0
- package/dist/review-server/middleware/hostOrigin.js +48 -0
- package/dist/review-server/middleware/reviewToken.js +7 -0
- package/dist/review-server/reviewServerPolicy.js +10 -0
- package/dist/review-server/server.js +568 -0
- package/dist/review-server/settingsApi.js +182 -0
- package/dist/review-server/walletIdentityResponse.js +13 -0
- package/dist/runtime/config.js +103 -0
- package/dist/runtime/localSettingsService.js +198 -0
- package/dist/runtime/logger.js +50 -0
- package/dist/runtime/reviewServerAcquire.js +128 -0
- package/dist/runtime/smokeMainnetRead.js +529 -0
- package/dist/runtime/smokeMainnetReadAssertions.js +308 -0
- package/dist/runtime/start.js +295 -0
- package/dist/runtime/suiEndpoint.js +97 -0
- package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
- package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
- package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
- package/docs/AGENT_BEHAVIOR.md +1 -1
- package/docs/AGENT_DEVELOPMENT_POLICY.md +20 -0
- package/docs/FRONTEND_POLICY.md +4 -3
- package/docs/MCP_SETUP.md +59 -7
- package/docs/MCP_TOOLS.md +1 -1
- package/docs/SDK_API.md +5 -1
- package/package.json +3 -2
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { assertNoForbiddenMcpFields } from "../action/forbiddenFields.js";
|
|
6
|
+
import { parseLifecycleValidatedReviewState } from "../action/reviewStateValidation.js";
|
|
7
|
+
import { actionPlanSchema, executionResultSchema } from "../action/schemas.js";
|
|
8
|
+
import { parseSuiAddress } from "../suiAddress.js";
|
|
9
|
+
import { SqlitePreferencesRepository } from "../preferences/sqlitePreferencesRepository.js";
|
|
10
|
+
import { SqliteLocalDataService } from "./localDataService.js";
|
|
11
|
+
import { ActivityStoreReadError, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS, REVIEW_ACTIVITY_LOW_SAMPLE_THRESHOLD } from "./activityStore.js";
|
|
12
|
+
import { configureDatabase, initializeDatabase } from "./sqliteActivityStoreSchema.js";
|
|
13
|
+
import { ActivityStoreError } from "./sqliteActivityStoreTypes.js";
|
|
14
|
+
import { EXTERNAL_ACTIVITY_RELATIONSHIPS, EXTERNAL_ACTIVITY_STATUSES, INTERNAL_SESSION_STATUSES, REVIEW_STATE_STATUSES, REVIEW_TRANSITION_EVENTS, asAccountSource, asInternalSessionStatus, asReviewTransitionEvent, asString, assertDateRange, canAdvanceReviewExecution, coinMetadataCacheRecordFromRow, countMap, emptyExternalActivitySummaryStats, emptyReviewFunnelSummary, externalActivityScanFromRow, externalActivitySummaryResult, externalActivityTransactionFromRow, extractRequestedIntent, isSameReviewExecution, normalizeExternalActivityLimit, normalizeListLimit, nullableSeconds, parseActionPlanEvidence, parseEvidenceJson, parseIsoTimestamp, parseOptionalIsoTimestamp, reasonForReviewState, reviewActivityListResult, reviewActivityRowFromStorage, reviewFunnelResult, reviewSessionWhere, serializeExternalActivityTransactionDetail, serializeJson, serializeOptionalJson } from "./sqliteActivityStoreRows.js";
|
|
15
|
+
export { ActivityStoreError };
|
|
16
|
+
const ACTIVE_ACCOUNT_SINGLETON_ID = 1;
|
|
17
|
+
export const DATA_DIR_ENV = "SAY_UR_INTENT_DATA_DIR";
|
|
18
|
+
export const ACTIVITY_DATABASE_FILENAME = "say-ur-intent.sqlite";
|
|
19
|
+
export class SqliteActivityStore {
|
|
20
|
+
db;
|
|
21
|
+
validateAdapterLifecycle;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.validateAdapterLifecycle = options.validateAdapterLifecycle;
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(dirname(options.databasePath), { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new ActivityStoreError(`Could not create the local activity data directory. Check directory permissions or set ${DATA_DIR_ENV}.`);
|
|
29
|
+
}
|
|
30
|
+
this.db = new Database(options.databasePath);
|
|
31
|
+
try {
|
|
32
|
+
configureDatabase(this.db);
|
|
33
|
+
initializeDatabase(this.db);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
this.db.close();
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async upsertAccount(address, source, now = new Date()) {
|
|
41
|
+
return this.upsertAccountSync(address, source, now.toISOString());
|
|
42
|
+
}
|
|
43
|
+
async getKnownAccount(address) {
|
|
44
|
+
const normalized = parseSuiAddress(address);
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
throw new ActivityStoreReadError("input_invalid", "Invalid account address", { field: "account" });
|
|
47
|
+
}
|
|
48
|
+
return this.getAccountByAddressSync(normalized);
|
|
49
|
+
}
|
|
50
|
+
async setActiveAccount(address, source, now = new Date(), wallet) {
|
|
51
|
+
const timestamp = now.toISOString();
|
|
52
|
+
return this.db.transaction(() => {
|
|
53
|
+
const account = this.upsertAccountSync(address, source, timestamp);
|
|
54
|
+
this.db
|
|
55
|
+
.prepare(`INSERT INTO active_account_context (id, account_id, source, set_at, wallet_name, wallet_id)
|
|
56
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
57
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
58
|
+
account_id = excluded.account_id,
|
|
59
|
+
source = excluded.source,
|
|
60
|
+
set_at = excluded.set_at,
|
|
61
|
+
wallet_name = excluded.wallet_name,
|
|
62
|
+
wallet_id = excluded.wallet_id`)
|
|
63
|
+
.run(ACTIVE_ACCOUNT_SINGLETON_ID, account.id, source, timestamp, wallet?.name ?? null, wallet?.id ?? null);
|
|
64
|
+
return {
|
|
65
|
+
accountId: account.id,
|
|
66
|
+
address: account.address,
|
|
67
|
+
source,
|
|
68
|
+
setAt: timestamp,
|
|
69
|
+
...(wallet?.name ? { walletName: wallet.name } : {}),
|
|
70
|
+
...(wallet?.id ? { walletId: wallet.id } : {})
|
|
71
|
+
};
|
|
72
|
+
})();
|
|
73
|
+
}
|
|
74
|
+
async getActiveAccount() {
|
|
75
|
+
return this.getActiveAccountSync();
|
|
76
|
+
}
|
|
77
|
+
async clearActiveAccount(now = new Date()) {
|
|
78
|
+
this.db
|
|
79
|
+
.prepare(`INSERT INTO active_account_context (id, account_id, source, set_at)
|
|
80
|
+
VALUES (?, NULL, 'cleared', ?)
|
|
81
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
82
|
+
account_id = NULL,
|
|
83
|
+
source = 'cleared',
|
|
84
|
+
set_at = excluded.set_at`)
|
|
85
|
+
.run(ACTIVE_ACCOUNT_SINGLETON_ID, now.toISOString());
|
|
86
|
+
}
|
|
87
|
+
async recordReviewSession(input) {
|
|
88
|
+
const plan = parseActionPlanEvidence(input.plan);
|
|
89
|
+
const planJson = serializeJson(plan);
|
|
90
|
+
const intentJson = serializeOptionalJson(extractRequestedIntent(plan));
|
|
91
|
+
this.db
|
|
92
|
+
.transaction(() => {
|
|
93
|
+
this.db
|
|
94
|
+
.prepare(`INSERT INTO review_sessions
|
|
95
|
+
(id, plan_id, action_kind, adapter_id, protocol, current_status,
|
|
96
|
+
plan_json, intent_json, created_at, updated_at)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
98
|
+
.run(input.reviewSessionId, plan.id, plan.actionKind, plan.adapterId, plan.protocol, input.currentStatus, planJson, intentJson, input.createdAt, input.createdAt);
|
|
99
|
+
this.insertReviewTransition({
|
|
100
|
+
reviewSessionId: input.reviewSessionId,
|
|
101
|
+
event: "created",
|
|
102
|
+
toStatus: input.currentStatus,
|
|
103
|
+
transitionedAt: input.createdAt
|
|
104
|
+
});
|
|
105
|
+
})();
|
|
106
|
+
}
|
|
107
|
+
async recordReviewTransition(input) {
|
|
108
|
+
this.db
|
|
109
|
+
.transaction(() => {
|
|
110
|
+
const accountId = input.account
|
|
111
|
+
? this.upsertAccountSync(input.account, "review_execution", input.transitionedAt).id
|
|
112
|
+
: null;
|
|
113
|
+
if (accountId !== null) {
|
|
114
|
+
this.assertReviewSessionAccount(input.reviewSessionId, accountId);
|
|
115
|
+
}
|
|
116
|
+
this.insertReviewTransition({ ...input, accountId });
|
|
117
|
+
this.db
|
|
118
|
+
.prepare(`UPDATE review_sessions
|
|
119
|
+
SET current_status = ?, account_id = COALESCE(account_id, ?), updated_at = ?
|
|
120
|
+
WHERE id = ?`)
|
|
121
|
+
.run(input.toStatus, accountId, input.transitionedAt, input.reviewSessionId);
|
|
122
|
+
})();
|
|
123
|
+
}
|
|
124
|
+
async recordReviewStateSnapshot(input) {
|
|
125
|
+
const parsedState = parseLifecycleValidatedReviewState(input.state, this.validateAdapterLifecycle);
|
|
126
|
+
const stateJson = serializeJson(parsedState);
|
|
127
|
+
this.db
|
|
128
|
+
.transaction(() => {
|
|
129
|
+
const account = this.upsertAccountSync(parsedState.account, "review_execution", input.recordedAt);
|
|
130
|
+
this.assertReviewSessionAccount(input.reviewSessionId, account.id);
|
|
131
|
+
this.db
|
|
132
|
+
.prepare(`INSERT INTO review_state_snapshots
|
|
133
|
+
(review_session_id, plan_id, account_id, status, blocked_reason, refresh_reason,
|
|
134
|
+
state_json, updated_at, recorded_at)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
136
|
+
.run(input.reviewSessionId, parsedState.planId, account.id, parsedState.status, "blockedReason" in parsedState ? parsedState.blockedReason : null, "refreshReason" in parsedState ? parsedState.refreshReason : null, stateJson, parsedState.updatedAt, input.recordedAt);
|
|
137
|
+
this.insertReviewTransition({
|
|
138
|
+
reviewSessionId: input.reviewSessionId,
|
|
139
|
+
event: "state_computed",
|
|
140
|
+
fromStatus: input.fromStatus,
|
|
141
|
+
toStatus: parsedState.status,
|
|
142
|
+
accountId: account.id,
|
|
143
|
+
reason: reasonForReviewState(parsedState),
|
|
144
|
+
transitionedAt: input.recordedAt
|
|
145
|
+
});
|
|
146
|
+
this.db
|
|
147
|
+
.prepare(`UPDATE review_sessions
|
|
148
|
+
SET current_status = ?, account_id = ?, updated_at = ?
|
|
149
|
+
WHERE id = ?`)
|
|
150
|
+
.run(parsedState.status, account.id, input.recordedAt, input.reviewSessionId);
|
|
151
|
+
})();
|
|
152
|
+
}
|
|
153
|
+
async recordReviewExecution(input) {
|
|
154
|
+
const resultJson = serializeJson(input.result);
|
|
155
|
+
this.db
|
|
156
|
+
.transaction(() => {
|
|
157
|
+
const account = this.upsertAccountSync(input.account, "review_execution", input.recordedAt);
|
|
158
|
+
this.assertReviewSessionAccount(input.reviewSessionId, account.id);
|
|
159
|
+
const existing = this.getReviewExecutionStorageRow(input.reviewSessionId);
|
|
160
|
+
if (existing) {
|
|
161
|
+
if (isSameReviewExecution(existing, account.id, input)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!canAdvanceReviewExecution(existing, account.id, input)) {
|
|
165
|
+
throw new ActivityStoreError(`Conflicting review execution evidence: ${input.reviewSessionId}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
this.db
|
|
169
|
+
.prepare(`INSERT INTO review_executions
|
|
170
|
+
(review_session_id, plan_id, account_id, status, tx_digest, explorer_url,
|
|
171
|
+
failure_reason, result_json, recorded_at, updated_at)
|
|
172
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
173
|
+
ON CONFLICT(review_session_id) DO UPDATE SET
|
|
174
|
+
plan_id = excluded.plan_id,
|
|
175
|
+
account_id = excluded.account_id,
|
|
176
|
+
status = excluded.status,
|
|
177
|
+
tx_digest = excluded.tx_digest,
|
|
178
|
+
explorer_url = excluded.explorer_url,
|
|
179
|
+
failure_reason = excluded.failure_reason,
|
|
180
|
+
result_json = excluded.result_json,
|
|
181
|
+
updated_at = excluded.updated_at`)
|
|
182
|
+
.run(input.reviewSessionId, input.planId, account.id, input.status, input.txDigest ?? null, input.explorerUrl ?? null, input.failureReason ?? null, resultJson, input.recordedAt, input.recordedAt);
|
|
183
|
+
this.insertReviewTransition({
|
|
184
|
+
reviewSessionId: input.reviewSessionId,
|
|
185
|
+
event: "result_recorded",
|
|
186
|
+
fromStatus: input.fromStatus,
|
|
187
|
+
toStatus: input.status,
|
|
188
|
+
accountId: account.id,
|
|
189
|
+
reason: input.failureReason,
|
|
190
|
+
transitionedAt: input.recordedAt
|
|
191
|
+
});
|
|
192
|
+
this.db
|
|
193
|
+
.prepare(`UPDATE review_sessions
|
|
194
|
+
SET current_status = ?, account_id = ?, updated_at = ?
|
|
195
|
+
WHERE id = ?`)
|
|
196
|
+
.run(input.status, account.id, input.recordedAt, input.reviewSessionId);
|
|
197
|
+
})();
|
|
198
|
+
const recorded = await this.getReviewExecution(input.reviewSessionId);
|
|
199
|
+
if (!recorded) {
|
|
200
|
+
throw new ActivityStoreError(`Review execution was not recorded: ${input.reviewSessionId}`);
|
|
201
|
+
}
|
|
202
|
+
return recorded;
|
|
203
|
+
}
|
|
204
|
+
getReviewExecutionStorageRow(reviewSessionId) {
|
|
205
|
+
return this.db
|
|
206
|
+
.prepare(`SELECT review_session_id, plan_id, account_id, status, tx_digest, explorer_url, failure_reason
|
|
207
|
+
FROM review_executions
|
|
208
|
+
WHERE review_session_id = ?`)
|
|
209
|
+
.get(reviewSessionId);
|
|
210
|
+
}
|
|
211
|
+
async getReviewExecution(reviewSessionId) {
|
|
212
|
+
const row = this.db
|
|
213
|
+
.prepare(`SELECT r.review_session_id, r.plan_id, r.account_id, a.sui_address AS account,
|
|
214
|
+
r.status, r.tx_digest, r.explorer_url, r.failure_reason, r.recorded_at, r.updated_at
|
|
215
|
+
FROM review_executions r
|
|
216
|
+
JOIN accounts a ON a.id = r.account_id
|
|
217
|
+
WHERE r.review_session_id = ?`)
|
|
218
|
+
.get(reviewSessionId);
|
|
219
|
+
return row
|
|
220
|
+
? {
|
|
221
|
+
reviewSessionId: asString(row.review_session_id),
|
|
222
|
+
planId: asString(row.plan_id),
|
|
223
|
+
accountId: row.account_id,
|
|
224
|
+
account: asString(row.account),
|
|
225
|
+
status: asString(row.status),
|
|
226
|
+
txDigest: row.tx_digest === null ? undefined : asString(row.tx_digest),
|
|
227
|
+
explorerUrl: row.explorer_url === null ? undefined : asString(row.explorer_url),
|
|
228
|
+
failureReason: row.failure_reason === null ? undefined : asString(row.failure_reason),
|
|
229
|
+
recordedAt: asString(row.recorded_at),
|
|
230
|
+
updatedAt: asString(row.updated_at)
|
|
231
|
+
}
|
|
232
|
+
: undefined;
|
|
233
|
+
}
|
|
234
|
+
async listReviewActivity(filter) {
|
|
235
|
+
const from = parseOptionalIsoTimestamp(filter.from, "from");
|
|
236
|
+
const to = parseOptionalIsoTimestamp(filter.to, "to");
|
|
237
|
+
assertDateRange(from, to);
|
|
238
|
+
const limit = normalizeListLimit(filter.limit);
|
|
239
|
+
const scope = this.resolveReviewActivityScope(filter);
|
|
240
|
+
if (scope.accountId === undefined) {
|
|
241
|
+
return reviewActivityListResult(scope, from, to, [], false, 0);
|
|
242
|
+
}
|
|
243
|
+
const { whereSql, params } = reviewSessionWhere(scope.accountId, from, to, filter.status);
|
|
244
|
+
const totalRow = this.db
|
|
245
|
+
.prepare(`SELECT COUNT(*) AS count FROM review_sessions rs ${whereSql}`)
|
|
246
|
+
.get(...params);
|
|
247
|
+
const rows = this.db
|
|
248
|
+
.prepare(`SELECT rs.id AS review_session_id, rs.plan_id, rs.action_kind, rs.adapter_id, rs.protocol,
|
|
249
|
+
rs.current_status, a.sui_address AS account, rs.created_at, rs.updated_at,
|
|
250
|
+
re.status AS execution_status, re.tx_digest,
|
|
251
|
+
(SELECT COUNT(*) FROM review_state_snapshots s WHERE s.review_session_id = rs.id) AS snapshot_count,
|
|
252
|
+
(SELECT COUNT(*) FROM review_status_transitions t WHERE t.review_session_id = rs.id) AS transition_count
|
|
253
|
+
FROM review_sessions rs
|
|
254
|
+
JOIN accounts a ON a.id = rs.account_id
|
|
255
|
+
LEFT JOIN review_executions re ON re.review_session_id = rs.id
|
|
256
|
+
${whereSql}
|
|
257
|
+
ORDER BY rs.created_at DESC, rs.id DESC
|
|
258
|
+
LIMIT ?`)
|
|
259
|
+
.all(...params, limit + 1);
|
|
260
|
+
const truncated = rows.length > limit;
|
|
261
|
+
const activities = rows.slice(0, limit).map(reviewActivityRowFromStorage);
|
|
262
|
+
return reviewActivityListResult(scope, from, to, activities, truncated, totalRow.count);
|
|
263
|
+
}
|
|
264
|
+
async summarizeReviewFunnel(filter) {
|
|
265
|
+
const from = parseOptionalIsoTimestamp(filter.from, "from");
|
|
266
|
+
const to = parseOptionalIsoTimestamp(filter.to, "to");
|
|
267
|
+
assertDateRange(from, to);
|
|
268
|
+
const scope = this.resolveReviewActivityScope(filter);
|
|
269
|
+
if (scope.accountId === undefined) {
|
|
270
|
+
return reviewFunnelResult(scope, from, to, emptyReviewFunnelSummary(), 0);
|
|
271
|
+
}
|
|
272
|
+
const { whereSql, params } = reviewSessionWhere(scope.accountId, from, to);
|
|
273
|
+
const total = this.db
|
|
274
|
+
.prepare(`SELECT COUNT(*) AS count FROM review_sessions rs ${whereSql}`)
|
|
275
|
+
.get(...params).count;
|
|
276
|
+
const eventCounts = this.db
|
|
277
|
+
.prepare(`SELECT t.event AS key, COUNT(DISTINCT t.review_session_id) AS count
|
|
278
|
+
FROM review_status_transitions t
|
|
279
|
+
JOIN review_sessions rs ON rs.id = t.review_session_id
|
|
280
|
+
${whereSql}
|
|
281
|
+
GROUP BY t.event`)
|
|
282
|
+
.all(...params);
|
|
283
|
+
const currentStatusCounts = this.db
|
|
284
|
+
.prepare(`SELECT rs.current_status AS key, COUNT(*) AS count
|
|
285
|
+
FROM review_sessions rs
|
|
286
|
+
${whereSql}
|
|
287
|
+
GROUP BY rs.current_status`)
|
|
288
|
+
.all(...params);
|
|
289
|
+
const reachedStateCounts = this.db
|
|
290
|
+
.prepare(`SELECT t.to_status AS key, COUNT(DISTINCT t.review_session_id) AS count
|
|
291
|
+
FROM review_status_transitions t
|
|
292
|
+
JOIN review_sessions rs ON rs.id = t.review_session_id
|
|
293
|
+
${whereSql}
|
|
294
|
+
AND t.to_status IN ('ready_for_wallet_review', 'blocked', 'refresh_required')
|
|
295
|
+
GROUP BY t.to_status`)
|
|
296
|
+
.all(...params);
|
|
297
|
+
const signedPending = this.db
|
|
298
|
+
.prepare(`SELECT COUNT(DISTINCT t.review_session_id) AS count
|
|
299
|
+
FROM review_status_transitions t
|
|
300
|
+
JOIN review_sessions rs ON rs.id = t.review_session_id
|
|
301
|
+
${whereSql}
|
|
302
|
+
AND t.event = 'result_recorded'
|
|
303
|
+
AND t.to_status = 'signed_pending_result'`)
|
|
304
|
+
.get(...params).count;
|
|
305
|
+
const expiredBeforeResult = this.db
|
|
306
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
307
|
+
FROM review_sessions rs
|
|
308
|
+
LEFT JOIN review_executions re ON re.review_session_id = rs.id
|
|
309
|
+
${whereSql}
|
|
310
|
+
AND rs.current_status = 'expired'
|
|
311
|
+
AND re.review_session_id IS NULL`)
|
|
312
|
+
.get(...params).count;
|
|
313
|
+
const timing = this.db
|
|
314
|
+
.prepare(`WITH scoped AS (
|
|
315
|
+
SELECT rs.id, rs.created_at
|
|
316
|
+
FROM review_sessions rs
|
|
317
|
+
${whereSql}
|
|
318
|
+
),
|
|
319
|
+
signed AS (
|
|
320
|
+
SELECT review_session_id, MIN(transitioned_at) AS signed_at
|
|
321
|
+
FROM review_status_transitions
|
|
322
|
+
WHERE event = 'result_recorded' AND to_status = 'signed_pending_result'
|
|
323
|
+
GROUP BY review_session_id
|
|
324
|
+
),
|
|
325
|
+
opened AS (
|
|
326
|
+
SELECT review_session_id, MIN(transitioned_at) AS opened_at
|
|
327
|
+
FROM review_status_transitions
|
|
328
|
+
WHERE event = 'opened'
|
|
329
|
+
GROUP BY review_session_id
|
|
330
|
+
)
|
|
331
|
+
SELECT
|
|
332
|
+
AVG((julianday(signed.signed_at) - julianday(scoped.created_at)) * 86400.0) AS avg_created_to_signed,
|
|
333
|
+
AVG(
|
|
334
|
+
CASE
|
|
335
|
+
WHEN opened.opened_at IS NULL THEN NULL
|
|
336
|
+
ELSE (julianday(signed.signed_at) - julianday(opened.opened_at)) * 86400.0
|
|
337
|
+
END
|
|
338
|
+
) AS avg_opened_to_signed
|
|
339
|
+
FROM scoped
|
|
340
|
+
JOIN signed ON signed.review_session_id = scoped.id
|
|
341
|
+
LEFT JOIN opened ON opened.review_session_id = scoped.id`)
|
|
342
|
+
.get(...params);
|
|
343
|
+
const currentStatus = countMap(INTERNAL_SESSION_STATUSES, currentStatusCounts);
|
|
344
|
+
const reachedStates = countMap(REVIEW_STATE_STATUSES, reachedStateCounts);
|
|
345
|
+
const events = countMap(REVIEW_TRANSITION_EVENTS, eventCounts);
|
|
346
|
+
const summary = {
|
|
347
|
+
total,
|
|
348
|
+
opened: events.opened,
|
|
349
|
+
walletConnected: events.wallet_connected,
|
|
350
|
+
stateComputed: events.state_computed,
|
|
351
|
+
currentStatusCounts: currentStatus,
|
|
352
|
+
everReachedReviewStateCounts: reachedStates,
|
|
353
|
+
signedPending,
|
|
354
|
+
success: currentStatus.success,
|
|
355
|
+
failure: currentStatus.failure,
|
|
356
|
+
expiredBeforeResult,
|
|
357
|
+
avgCreatedToSignedSeconds: nullableSeconds(timing.avg_created_to_signed),
|
|
358
|
+
avgOpenedToSignedSeconds: nullableSeconds(timing.avg_opened_to_signed)
|
|
359
|
+
};
|
|
360
|
+
return reviewFunnelResult(scope, from, to, summary, total);
|
|
361
|
+
}
|
|
362
|
+
async getReviewSessionDetail(input) {
|
|
363
|
+
const scope = this.resolveReviewActivityScope({ account: input.account });
|
|
364
|
+
if (scope.accountId === undefined) {
|
|
365
|
+
throw new ActivityStoreReadError("session_not_found", "Review session not found", {
|
|
366
|
+
reviewSessionId: input.reviewSessionId
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const row = this.db
|
|
370
|
+
.prepare(`SELECT rs.id AS review_session_id, rs.plan_id, rs.action_kind, rs.adapter_id, rs.protocol,
|
|
371
|
+
rs.current_status, a.sui_address AS account, rs.created_at, rs.updated_at,
|
|
372
|
+
rs.plan_json, rs.intent_json,
|
|
373
|
+
re.status AS execution_status, re.tx_digest, re.explorer_url, re.failure_reason,
|
|
374
|
+
re.recorded_at AS execution_recorded_at, re.updated_at AS execution_updated_at,
|
|
375
|
+
re.result_json
|
|
376
|
+
FROM review_sessions rs
|
|
377
|
+
JOIN accounts a ON a.id = rs.account_id
|
|
378
|
+
LEFT JOIN review_executions re ON re.review_session_id = rs.id
|
|
379
|
+
WHERE rs.id = ? AND rs.account_id = ?`)
|
|
380
|
+
.get(input.reviewSessionId, scope.accountId);
|
|
381
|
+
if (!row) {
|
|
382
|
+
throw new ActivityStoreReadError("session_not_found", "Review session not found", {
|
|
383
|
+
reviewSessionId: input.reviewSessionId
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const snapshotRows = this.db
|
|
387
|
+
.prepare(`SELECT s.id, s.plan_id, a.sui_address AS account, s.status, s.blocked_reason, s.refresh_reason,
|
|
388
|
+
s.state_json, s.updated_at, s.recorded_at
|
|
389
|
+
FROM review_state_snapshots s
|
|
390
|
+
JOIN accounts a ON a.id = s.account_id
|
|
391
|
+
WHERE s.review_session_id = ? AND s.account_id = ?
|
|
392
|
+
ORDER BY s.recorded_at ASC, s.id ASC
|
|
393
|
+
LIMIT ?`)
|
|
394
|
+
.all(input.reviewSessionId, scope.accountId, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS + 1);
|
|
395
|
+
const transitionRows = this.db
|
|
396
|
+
.prepare(`SELECT t.id, t.event, t.from_status, t.to_status, a.sui_address AS account,
|
|
397
|
+
t.reason, t.transitioned_at
|
|
398
|
+
FROM review_status_transitions t
|
|
399
|
+
LEFT JOIN accounts a ON a.id = t.account_id
|
|
400
|
+
WHERE t.review_session_id = ? AND (t.account_id IS NULL OR t.account_id = ?)
|
|
401
|
+
ORDER BY t.transitioned_at ASC, t.id ASC
|
|
402
|
+
LIMIT ?`)
|
|
403
|
+
.all(input.reviewSessionId, scope.accountId, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS + 1);
|
|
404
|
+
const recordCount = this.db
|
|
405
|
+
.prepare(`SELECT COUNT(*) AS count FROM review_sessions rs WHERE rs.account_id = ?`)
|
|
406
|
+
.get(scope.accountId).count;
|
|
407
|
+
const snapshotsTruncated = snapshotRows.length > REVIEW_ACTIVITY_DETAIL_MAX_ITEMS;
|
|
408
|
+
const transitionsTruncated = transitionRows.length > REVIEW_ACTIVITY_DETAIL_MAX_ITEMS;
|
|
409
|
+
const planJson = parseEvidenceJson(row.plan_json, input.reviewSessionId, "plan_json", actionPlanSchema);
|
|
410
|
+
const intentJson = row.intent_json === null
|
|
411
|
+
? undefined
|
|
412
|
+
: parseEvidenceJson(row.intent_json, input.reviewSessionId, "intent_json");
|
|
413
|
+
const execution = row.execution_status === null
|
|
414
|
+
? undefined
|
|
415
|
+
: {
|
|
416
|
+
reviewSessionId: asString(row.review_session_id),
|
|
417
|
+
planId: asString(row.plan_id),
|
|
418
|
+
accountId: scope.accountId,
|
|
419
|
+
account: asString(row.account),
|
|
420
|
+
status: asString(row.execution_status),
|
|
421
|
+
txDigest: row.tx_digest === null ? undefined : asString(row.tx_digest),
|
|
422
|
+
explorerUrl: row.explorer_url === null ? undefined : asString(row.explorer_url),
|
|
423
|
+
failureReason: row.failure_reason === null ? undefined : asString(row.failure_reason),
|
|
424
|
+
recordedAt: asString(row.execution_recorded_at),
|
|
425
|
+
updatedAt: asString(row.execution_updated_at),
|
|
426
|
+
resultJson: parseEvidenceJson(row.result_json, input.reviewSessionId, "result_json", executionResultSchema)
|
|
427
|
+
};
|
|
428
|
+
return {
|
|
429
|
+
dataScope: {
|
|
430
|
+
account: scope.account,
|
|
431
|
+
recordCount
|
|
432
|
+
},
|
|
433
|
+
accountSource: scope.accountSource,
|
|
434
|
+
lowSampleWarning: recordCount < REVIEW_ACTIVITY_LOW_SAMPLE_THRESHOLD,
|
|
435
|
+
lowSampleThreshold: REVIEW_ACTIVITY_LOW_SAMPLE_THRESHOLD,
|
|
436
|
+
session: {
|
|
437
|
+
reviewSessionId: asString(row.review_session_id),
|
|
438
|
+
planId: asString(row.plan_id),
|
|
439
|
+
actionKind: asString(row.action_kind),
|
|
440
|
+
adapterId: asString(row.adapter_id),
|
|
441
|
+
protocol: asString(row.protocol),
|
|
442
|
+
currentStatus: asInternalSessionStatus(row.current_status),
|
|
443
|
+
account: asString(row.account),
|
|
444
|
+
createdAt: asString(row.created_at),
|
|
445
|
+
updatedAt: asString(row.updated_at)
|
|
446
|
+
},
|
|
447
|
+
planJson,
|
|
448
|
+
intentJson,
|
|
449
|
+
stateSnapshots: snapshotRows.slice(0, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS).map((snapshot) => ({
|
|
450
|
+
id: snapshot.id,
|
|
451
|
+
planId: asString(snapshot.plan_id),
|
|
452
|
+
account: asString(snapshot.account),
|
|
453
|
+
status: asString(snapshot.status),
|
|
454
|
+
blockedReason: snapshot.blocked_reason === null ? undefined : asString(snapshot.blocked_reason),
|
|
455
|
+
refreshReason: snapshot.refresh_reason === null ? undefined : asString(snapshot.refresh_reason),
|
|
456
|
+
stateJson: this.parseReviewStateEvidenceJson(snapshot.state_json, input.reviewSessionId, "state_json"),
|
|
457
|
+
updatedAt: asString(snapshot.updated_at),
|
|
458
|
+
recordedAt: asString(snapshot.recorded_at)
|
|
459
|
+
})),
|
|
460
|
+
transitions: transitionRows.slice(0, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS).map((transitionRow) => ({
|
|
461
|
+
id: transitionRow.id,
|
|
462
|
+
event: asReviewTransitionEvent(transitionRow.event),
|
|
463
|
+
fromStatus: transitionRow.from_status === null ? undefined : asString(transitionRow.from_status),
|
|
464
|
+
toStatus: asString(transitionRow.to_status),
|
|
465
|
+
isNoOp: transitionRow.from_status !== null && asString(transitionRow.from_status) === asString(transitionRow.to_status),
|
|
466
|
+
account: transitionRow.account === null ? undefined : asString(transitionRow.account),
|
|
467
|
+
reason: transitionRow.reason === null ? undefined : asString(transitionRow.reason),
|
|
468
|
+
transitionedAt: asString(transitionRow.transitioned_at)
|
|
469
|
+
})),
|
|
470
|
+
execution,
|
|
471
|
+
truncated: {
|
|
472
|
+
activities: false,
|
|
473
|
+
snapshots: snapshotsTruncated,
|
|
474
|
+
transitions: transitionsTruncated
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async recordExternalActivityScan(input) {
|
|
479
|
+
try {
|
|
480
|
+
assertNoForbiddenMcpFields(input);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
throw new ActivityStoreReadError("input_invalid", "External activity scan contains forbidden fields", {
|
|
484
|
+
reason: "forbidden_field"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const account = await this.getKnownAccount(input.account);
|
|
488
|
+
if (!account) {
|
|
489
|
+
throw new ActivityStoreReadError("input_invalid", "External activity scan account is not a known wallet", {
|
|
490
|
+
reason: "account_not_known"
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
const fetchedAt = parseIsoTimestamp(input.fetchedAt, "fetchedAt");
|
|
494
|
+
const fromTimestamp = parseOptionalIsoTimestamp(input.fromTimestamp, "from");
|
|
495
|
+
const toTimestamp = parseOptionalIsoTimestamp(input.toTimestamp, "to");
|
|
496
|
+
assertDateRange(fromTimestamp, toTimestamp);
|
|
497
|
+
const record = this.db.transaction(() => {
|
|
498
|
+
this.db
|
|
499
|
+
.prepare(`INSERT INTO external_activity_scans
|
|
500
|
+
(scan_id, kind, account_id, relationship, input_digest, from_checkpoint, to_checkpoint,
|
|
501
|
+
from_timestamp, to_timestamp, limit_count, request_cursor, response_cursor, endpoint_host,
|
|
502
|
+
chain_identifier, fetched_at, stored_count, skipped_count, has_more, window_complete,
|
|
503
|
+
incomplete_reason)
|
|
504
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
505
|
+
.run(input.scanId, input.kind, account.id, input.relationship, input.inputDigest ?? null, input.fromCheckpoint ?? null, input.toCheckpoint ?? null, fromTimestamp ?? null, toTimestamp ?? null, input.limit, input.requestCursor ?? null, input.responseCursor ?? null, input.endpointHost, input.chainIdentifier, fetchedAt, 0, input.transactions.length, input.hasMore ? 1 : 0, input.windowComplete === null ? null : input.windowComplete ? 1 : 0, input.incompleteReason ?? null);
|
|
506
|
+
let storedCount = 0;
|
|
507
|
+
const skippedCount = input.skippedCount ?? 0;
|
|
508
|
+
const upsert = this.db.prepare(`INSERT INTO external_activity_transactions
|
|
509
|
+
(account_id, digest, relationship, checkpoint, timestamp, status, known_sender_account_id,
|
|
510
|
+
first_scan_id, last_scan_id, first_fetched_at, last_fetched_at, detail_json)
|
|
511
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
512
|
+
ON CONFLICT(account_id, digest, relationship) DO UPDATE SET
|
|
513
|
+
checkpoint = COALESCE(excluded.checkpoint, external_activity_transactions.checkpoint),
|
|
514
|
+
timestamp = COALESCE(excluded.timestamp, external_activity_transactions.timestamp),
|
|
515
|
+
status = excluded.status,
|
|
516
|
+
known_sender_account_id = COALESCE(excluded.known_sender_account_id, external_activity_transactions.known_sender_account_id),
|
|
517
|
+
last_scan_id = excluded.last_scan_id,
|
|
518
|
+
last_fetched_at = excluded.last_fetched_at,
|
|
519
|
+
detail_json = COALESCE(excluded.detail_json, external_activity_transactions.detail_json)`);
|
|
520
|
+
for (const transaction of input.transactions) {
|
|
521
|
+
if (transaction.knownSenderAccountId !== undefined && !this.accountIdExists(transaction.knownSenderAccountId)) {
|
|
522
|
+
throw new ActivityStoreReadError("input_invalid", "External activity sender account is not known", {
|
|
523
|
+
reason: "sender_account_not_known"
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
upsert.run(account.id, transaction.digest, transaction.relationship, transaction.checkpoint ?? null, transaction.timestamp ? parseIsoTimestamp(transaction.timestamp, "transaction.timestamp") : null, transaction.status, transaction.knownSenderAccountId ?? null, input.scanId, input.scanId, fetchedAt, fetchedAt, transaction.details === undefined
|
|
527
|
+
? null
|
|
528
|
+
: serializeExternalActivityTransactionDetail(transaction.details, account.address));
|
|
529
|
+
storedCount += 1;
|
|
530
|
+
}
|
|
531
|
+
this.db
|
|
532
|
+
.prepare(`UPDATE external_activity_scans
|
|
533
|
+
SET stored_count = ?, skipped_count = ?
|
|
534
|
+
WHERE scan_id = ?`)
|
|
535
|
+
.run(storedCount, skippedCount + input.transactions.length - storedCount, input.scanId);
|
|
536
|
+
return this.externalActivityScanById(input.scanId);
|
|
537
|
+
})();
|
|
538
|
+
if (!record) {
|
|
539
|
+
throw new ActivityStoreReadError("internal_error", "External activity scan was not recorded", {
|
|
540
|
+
scanId: input.scanId
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return record;
|
|
544
|
+
}
|
|
545
|
+
async summarizeExternalActivity(filter) {
|
|
546
|
+
const from = parseOptionalIsoTimestamp(filter.from, "from");
|
|
547
|
+
const to = parseOptionalIsoTimestamp(filter.to, "to");
|
|
548
|
+
assertDateRange(from, to);
|
|
549
|
+
const limit = normalizeExternalActivityLimit(filter.limit);
|
|
550
|
+
const scope = this.resolveReviewActivityScope(filter);
|
|
551
|
+
if (scope.accountId === undefined) {
|
|
552
|
+
return externalActivitySummaryResult(scope, from, to, [], false, emptyExternalActivitySummaryStats());
|
|
553
|
+
}
|
|
554
|
+
const where = ["eat.account_id = ?"];
|
|
555
|
+
const params = [scope.accountId];
|
|
556
|
+
if (from !== undefined) {
|
|
557
|
+
where.push("eat.timestamp >= ?");
|
|
558
|
+
params.push(from);
|
|
559
|
+
}
|
|
560
|
+
if (to !== undefined) {
|
|
561
|
+
where.push("eat.timestamp <= ?");
|
|
562
|
+
params.push(to);
|
|
563
|
+
}
|
|
564
|
+
const whereSql = `WHERE ${where.join(" AND ")}`;
|
|
565
|
+
const total = this.db
|
|
566
|
+
.prepare(`SELECT COUNT(*) AS count FROM external_activity_transactions eat ${whereSql}`)
|
|
567
|
+
.get(...params).count;
|
|
568
|
+
const statusCounts = countMap(EXTERNAL_ACTIVITY_STATUSES, this.db
|
|
569
|
+
.prepare(`SELECT eat.status AS key, COUNT(*) AS count FROM external_activity_transactions eat ${whereSql} GROUP BY eat.status`)
|
|
570
|
+
.all(...params));
|
|
571
|
+
const relationshipCounts = countMap(EXTERNAL_ACTIVITY_RELATIONSHIPS, this.db
|
|
572
|
+
.prepare(`SELECT eat.relationship AS key, COUNT(*) AS count FROM external_activity_transactions eat ${whereSql} GROUP BY eat.relationship`)
|
|
573
|
+
.all(...params));
|
|
574
|
+
const timestampRow = this.db
|
|
575
|
+
.prepare(`SELECT MIN(eat.timestamp) AS earliest_timestamp, MAX(eat.timestamp) AS latest_timestamp
|
|
576
|
+
FROM external_activity_transactions eat
|
|
577
|
+
${whereSql}`)
|
|
578
|
+
.get(...params);
|
|
579
|
+
const rows = this.db
|
|
580
|
+
.prepare(`SELECT eat.account_id, a.sui_address AS account, eat.digest, eat.relationship,
|
|
581
|
+
eat.checkpoint, eat.timestamp, eat.status, eat.known_sender_account_id,
|
|
582
|
+
eat.first_scan_id, eat.last_scan_id, eat.first_fetched_at, eat.last_fetched_at,
|
|
583
|
+
last_scan.incomplete_reason AS last_scan_incomplete_reason,
|
|
584
|
+
eat.detail_json
|
|
585
|
+
FROM external_activity_transactions eat
|
|
586
|
+
JOIN accounts a ON a.id = eat.account_id
|
|
587
|
+
LEFT JOIN external_activity_scans last_scan ON last_scan.scan_id = eat.last_scan_id
|
|
588
|
+
${whereSql}
|
|
589
|
+
ORDER BY
|
|
590
|
+
CASE WHEN eat.checkpoint IS NULL THEN 0 ELSE 1 END DESC,
|
|
591
|
+
CAST(eat.checkpoint AS INTEGER) DESC,
|
|
592
|
+
COALESCE(eat.timestamp, '') DESC,
|
|
593
|
+
eat.digest DESC
|
|
594
|
+
LIMIT ?`)
|
|
595
|
+
.all(...params, limit + 1);
|
|
596
|
+
return externalActivitySummaryResult(scope, from, to, rows.slice(0, limit).map(externalActivityTransactionFromRow), rows.length > limit, {
|
|
597
|
+
transactionCount: total,
|
|
598
|
+
statusCounts,
|
|
599
|
+
relationshipCounts,
|
|
600
|
+
earliestTimestamp: timestampRow.earliest_timestamp ?? undefined,
|
|
601
|
+
latestTimestamp: timestampRow.latest_timestamp ?? undefined
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
close() {
|
|
605
|
+
this.db.close();
|
|
606
|
+
}
|
|
607
|
+
createPreferencesRepository() {
|
|
608
|
+
return new SqlitePreferencesRepository(this.db);
|
|
609
|
+
}
|
|
610
|
+
createLocalDataService(options) {
|
|
611
|
+
return new SqliteLocalDataService(this.db, options, this.validateAdapterLifecycle);
|
|
612
|
+
}
|
|
613
|
+
createCoinMetadataCache() {
|
|
614
|
+
return new SqliteCoinMetadataCache(this.db);
|
|
615
|
+
}
|
|
616
|
+
upsertAccountSync(address, source, timestamp) {
|
|
617
|
+
const normalized = parseSuiAddress(address);
|
|
618
|
+
if (!normalized) {
|
|
619
|
+
throw new ActivityStoreError("Invalid Sui account address");
|
|
620
|
+
}
|
|
621
|
+
this.db
|
|
622
|
+
.prepare(`INSERT INTO accounts (sui_address, first_seen_at, last_used_at, first_source, last_source)
|
|
623
|
+
VALUES (?, ?, ?, ?, ?)
|
|
624
|
+
ON CONFLICT(sui_address) DO UPDATE SET
|
|
625
|
+
last_used_at = excluded.last_used_at,
|
|
626
|
+
last_source = excluded.last_source`)
|
|
627
|
+
.run(normalized, timestamp, timestamp, source, source);
|
|
628
|
+
const row = this.db
|
|
629
|
+
.prepare(`SELECT id, sui_address, first_seen_at, last_used_at, first_source, last_source
|
|
630
|
+
FROM accounts
|
|
631
|
+
WHERE sui_address = ?`)
|
|
632
|
+
.get(normalized);
|
|
633
|
+
if (!row) {
|
|
634
|
+
throw new ActivityStoreError(`Account was not recorded: ${normalized}`);
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
id: row.id,
|
|
638
|
+
address: asString(row.sui_address),
|
|
639
|
+
firstSeenAt: asString(row.first_seen_at),
|
|
640
|
+
lastUsedAt: asString(row.last_used_at),
|
|
641
|
+
firstSource: asAccountSource(row.first_source),
|
|
642
|
+
lastSource: asAccountSource(row.last_source)
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
parseReviewStateEvidenceJson(value, reviewSessionId, evidenceField) {
|
|
646
|
+
const parsed = parseEvidenceJson(value, reviewSessionId, evidenceField);
|
|
647
|
+
try {
|
|
648
|
+
return parseLifecycleValidatedReviewState(parsed, this.validateAdapterLifecycle);
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
throw new ActivityStoreReadError("internal_error", "Malformed activity JSON evidence", {
|
|
652
|
+
reviewSessionId,
|
|
653
|
+
evidenceField
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
getAccountByAddressSync(address) {
|
|
658
|
+
const row = this.db
|
|
659
|
+
.prepare(`SELECT id, sui_address, first_seen_at, last_used_at, first_source, last_source
|
|
660
|
+
FROM accounts
|
|
661
|
+
WHERE sui_address = ?`)
|
|
662
|
+
.get(address);
|
|
663
|
+
return row
|
|
664
|
+
? {
|
|
665
|
+
id: row.id,
|
|
666
|
+
address: asString(row.sui_address),
|
|
667
|
+
firstSeenAt: asString(row.first_seen_at),
|
|
668
|
+
lastUsedAt: asString(row.last_used_at),
|
|
669
|
+
firstSource: asAccountSource(row.first_source),
|
|
670
|
+
lastSource: asAccountSource(row.last_source)
|
|
671
|
+
}
|
|
672
|
+
: undefined;
|
|
673
|
+
}
|
|
674
|
+
accountIdExists(accountId) {
|
|
675
|
+
const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
|
|
676
|
+
return row !== undefined;
|
|
677
|
+
}
|
|
678
|
+
externalActivityScanById(scanId) {
|
|
679
|
+
const row = this.db
|
|
680
|
+
.prepare(`SELECT eas.scan_id, eas.kind, eas.account_id, a.sui_address AS account, eas.relationship,
|
|
681
|
+
eas.input_digest, eas.from_checkpoint, eas.to_checkpoint, eas.from_timestamp,
|
|
682
|
+
eas.to_timestamp, eas.limit_count, eas.request_cursor, eas.response_cursor,
|
|
683
|
+
eas.endpoint_host, eas.chain_identifier, eas.fetched_at, eas.stored_count,
|
|
684
|
+
eas.skipped_count, eas.has_more, eas.window_complete, eas.incomplete_reason
|
|
685
|
+
FROM external_activity_scans eas
|
|
686
|
+
JOIN accounts a ON a.id = eas.account_id
|
|
687
|
+
WHERE eas.scan_id = ?`)
|
|
688
|
+
.get(scanId);
|
|
689
|
+
return row ? externalActivityScanFromRow(row) : undefined;
|
|
690
|
+
}
|
|
691
|
+
insertReviewTransition(input) {
|
|
692
|
+
this.db
|
|
693
|
+
.prepare(`INSERT INTO review_status_transitions
|
|
694
|
+
(review_session_id, event, from_status, to_status, account_id, reason, transitioned_at)
|
|
695
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
696
|
+
.run(input.reviewSessionId, input.event, input.fromStatus ?? null, input.toStatus, input.accountId ?? null, input.reason ?? null, input.transitionedAt);
|
|
697
|
+
}
|
|
698
|
+
resolveReviewActivityScope(filter) {
|
|
699
|
+
if (filter.account) {
|
|
700
|
+
const normalized = parseSuiAddress(filter.account);
|
|
701
|
+
if (!normalized) {
|
|
702
|
+
throw new ActivityStoreReadError("input_invalid", "Invalid account filter", { field: "account" });
|
|
703
|
+
}
|
|
704
|
+
const row = this.db
|
|
705
|
+
.prepare("SELECT id FROM accounts WHERE sui_address = ?")
|
|
706
|
+
.get(normalized);
|
|
707
|
+
return {
|
|
708
|
+
account: normalized,
|
|
709
|
+
accountId: row?.id,
|
|
710
|
+
accountSource: "explicit_filter"
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const active = this.getActiveAccountSync();
|
|
714
|
+
if (!active) {
|
|
715
|
+
throw new ActivityStoreReadError("active_account_not_set", "Active account read context is not set", {
|
|
716
|
+
action: "connect_wallet_identity"
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
account: active.address,
|
|
721
|
+
accountId: active.accountId,
|
|
722
|
+
accountSource: "active_account_context"
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
getActiveAccountSync() {
|
|
726
|
+
const row = this.db
|
|
727
|
+
.prepare(`SELECT a.id AS account_id, a.sui_address AS address, c.source AS source, c.set_at AS set_at,
|
|
728
|
+
c.wallet_name AS wallet_name, c.wallet_id AS wallet_id
|
|
729
|
+
FROM active_account_context c
|
|
730
|
+
JOIN accounts a ON a.id = c.account_id
|
|
731
|
+
WHERE c.id = ? AND c.account_id IS NOT NULL`)
|
|
732
|
+
.get(ACTIVE_ACCOUNT_SINGLETON_ID);
|
|
733
|
+
return row
|
|
734
|
+
? {
|
|
735
|
+
accountId: row.account_id,
|
|
736
|
+
address: asString(row.address),
|
|
737
|
+
source: "wallet_identity",
|
|
738
|
+
setAt: asString(row.set_at),
|
|
739
|
+
...(row.wallet_name ? { walletName: row.wallet_name } : {}),
|
|
740
|
+
...(row.wallet_id ? { walletId: row.wallet_id } : {})
|
|
741
|
+
}
|
|
742
|
+
: undefined;
|
|
743
|
+
}
|
|
744
|
+
assertReviewSessionAccount(reviewSessionId, accountId) {
|
|
745
|
+
const row = this.db
|
|
746
|
+
.prepare("SELECT account_id FROM review_sessions WHERE id = ?")
|
|
747
|
+
.get(reviewSessionId);
|
|
748
|
+
if (!row) {
|
|
749
|
+
throw new ActivityStoreError(`Review session not found: ${reviewSessionId}`);
|
|
750
|
+
}
|
|
751
|
+
if (row.account_id !== null && row.account_id !== accountId) {
|
|
752
|
+
throw new ActivityStoreError(`Review session already belongs to a different account: ${reviewSessionId}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
class SqliteCoinMetadataCache {
|
|
757
|
+
db;
|
|
758
|
+
constructor(db) {
|
|
759
|
+
this.db = db;
|
|
760
|
+
}
|
|
761
|
+
async getCoinMetadata(input) {
|
|
762
|
+
const row = this.db
|
|
763
|
+
.prepare(`SELECT coin_type, chain_identifier, decimals, symbol, name, fetched_at, expires_at
|
|
764
|
+
FROM coin_metadata_cache
|
|
765
|
+
WHERE coin_type = ? AND chain_identifier = ?`)
|
|
766
|
+
.get(input.coinType, input.chainIdentifier);
|
|
767
|
+
if (!row) {
|
|
768
|
+
return { status: "miss" };
|
|
769
|
+
}
|
|
770
|
+
const record = coinMetadataCacheRecordFromRow(row);
|
|
771
|
+
return record.expiresAt > input.now.toISOString()
|
|
772
|
+
? { status: "hit", record }
|
|
773
|
+
: { status: "expired", record };
|
|
774
|
+
}
|
|
775
|
+
async setCoinMetadata(record) {
|
|
776
|
+
this.db
|
|
777
|
+
.prepare(`INSERT INTO coin_metadata_cache
|
|
778
|
+
(coin_type, chain_identifier, decimals, symbol, name, fetched_at, expires_at)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
780
|
+
ON CONFLICT(coin_type, chain_identifier) DO UPDATE SET
|
|
781
|
+
decimals = excluded.decimals,
|
|
782
|
+
symbol = excluded.symbol,
|
|
783
|
+
name = excluded.name,
|
|
784
|
+
fetched_at = excluded.fetched_at,
|
|
785
|
+
expires_at = excluded.expires_at`)
|
|
786
|
+
.run(record.coinType, record.chainIdentifier, record.decimals, record.symbol, record.name, record.fetchedAt, record.expiresAt);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
export function resolveActivityDatabasePath(env = process.env, platform = process.platform) {
|
|
790
|
+
const configured = env[DATA_DIR_ENV];
|
|
791
|
+
const dataDir = configured && configured.trim() ? configured : defaultDataDir(env, platform);
|
|
792
|
+
if (dataDir.includes("\0")) {
|
|
793
|
+
throw new ActivityStoreError(`${DATA_DIR_ENV} must not contain null bytes`);
|
|
794
|
+
}
|
|
795
|
+
return resolve(dataDir, ACTIVITY_DATABASE_FILENAME);
|
|
796
|
+
}
|
|
797
|
+
export function assertSqliteEngineAvailable() {
|
|
798
|
+
const db = new Database(":memory:");
|
|
799
|
+
try {
|
|
800
|
+
db.exec("CREATE TABLE engine_check (id INTEGER PRIMARY KEY)");
|
|
801
|
+
db.prepare("INSERT INTO engine_check (id) VALUES (?)").run(1);
|
|
802
|
+
const row = db.prepare("SELECT id FROM engine_check").get();
|
|
803
|
+
if (row?.id !== 1) {
|
|
804
|
+
throw new ActivityStoreError("better-sqlite3 smoke query failed");
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
db.close();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function defaultDataDir(env, platform) {
|
|
812
|
+
const home = homedir();
|
|
813
|
+
if (platform === "darwin") {
|
|
814
|
+
return resolve(home, "Library", "Application Support", "say-ur-intent");
|
|
815
|
+
}
|
|
816
|
+
if (platform === "win32") {
|
|
817
|
+
return resolve(env.APPDATA ?? resolve(home, "AppData", "Roaming"), "say-ur-intent");
|
|
818
|
+
}
|
|
819
|
+
return resolve(env.XDG_DATA_HOME ?? resolve(home, ".local", "share"), "say-ur-intent");
|
|
820
|
+
}
|