@totalreclaw/totalreclaw 3.3.12 → 3.3.13
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/SKILL.md +25 -8
- package/{billing-cache.ts → billing/billing-cache.ts} +1 -1
- package/{relay-headers.ts → billing/relay-headers.ts} +1 -1
- package/{tr-cli-export-helper.ts → cli/tr-cli-export-helper.ts} +5 -5
- package/{tr-cli.ts → cli/tr-cli.ts} +20 -20
- package/{contradiction-sync.ts → contradiction/contradiction-sync.ts} +13 -13
- package/{digest-sync.ts → digest/digest-sync.ts} +32 -2
- package/dist/{billing-cache.js → billing/billing-cache.js} +1 -1
- package/dist/{relay-headers.js → billing/relay-headers.js} +1 -1
- package/dist/{tr-cli-export-helper.js → cli/tr-cli-export-helper.js} +5 -5
- package/dist/{tr-cli.js → cli/tr-cli.js} +19 -19
- package/dist/{contradiction-sync.js → contradiction/contradiction-sync.js} +13 -13
- package/dist/{digest-sync.js → digest/digest-sync.js} +32 -2
- package/dist/{download-ux.js → embedding/download-ux.js} +1 -1
- package/dist/{reranker.js → embedding/reranker.js} +13 -4
- package/dist/{claims-helper.js → extraction/claims-helper.js} +54 -58
- package/dist/extraction/consolidation.js +170 -0
- package/dist/{extractor.js → extraction/extractor.js} +17 -17
- package/dist/extraction/importance-filter.js +38 -0
- package/dist/{semantic-dedup.js → extraction/semantic-dedup.js} +2 -2
- package/dist/{batch-gate.js → import/batch-gate.js} +1 -1
- package/dist/import/import-runtime.js +692 -0
- package/dist/import-adapters/chatgpt-adapter.js +7 -7
- package/dist/import-adapters/gemini-adapter.js +13 -13
- package/dist/import-adapters/mem0-adapter.js +10 -10
- package/dist/index.js +111 -1094
- package/dist/{llm-client.js → llm/llm-client.js} +2 -2
- package/dist/{hot-cache-wrapper.js → memory/hot-cache-wrapper.js} +5 -5
- package/dist/{memory-runtime.js → memory/memory-runtime.js} +1 -1
- package/dist/{pin.js → memory/pin.js} +18 -18
- package/dist/{retype-setscope.js → memory/retype-setscope.js} +19 -19
- package/dist/{credential-provider.js → pairing/credential-provider.js} +2 -2
- package/dist/{first-run.js → pairing/first-run.js} +1 -1
- package/dist/{onboarding-cli.js → pairing/onboarding-cli.js} +20 -20
- package/dist/{pair-cli-relay.js → pairing/pair-cli-relay.js} +10 -10
- package/dist/{pair-crypto.js → pairing/pair-crypto.js} +1 -1
- package/dist/{pair-http.js → pairing/pair-http.js} +3 -3
- package/dist/{pair-remote-client.js → pairing/pair-remote-client.js} +10 -10
- package/dist/{pair-session-store.js → pairing/pair-session-store.js} +4 -4
- package/dist/runtime/config-schema.js +59 -0
- package/dist/runtime/format-helpers.js +257 -0
- package/dist/runtime/types.js +11 -0
- package/dist/{confirm-indexed.js → subgraph/confirm-indexed.js} +1 -1
- package/dist/{subgraph-search.js → subgraph/subgraph-search.js} +14 -14
- package/dist/{subgraph-store.js → subgraph/subgraph-store.js} +6 -6
- package/{download-ux.ts → embedding/download-ux.ts} +1 -1
- package/{reranker.ts → embedding/reranker.ts} +13 -4
- package/{claims-helper.ts → extraction/claims-helper.ts} +52 -60
- package/{consolidation.ts → extraction/consolidation.ts} +63 -154
- package/{extractor.ts → extraction/extractor.ts} +18 -17
- package/extraction/importance-filter.ts +50 -0
- package/{semantic-dedup.ts → extraction/semantic-dedup.ts} +2 -2
- package/{batch-gate.ts → import/batch-gate.ts} +1 -1
- package/import/import-runtime.ts +853 -0
- package/import-adapters/chatgpt-adapter.ts +7 -7
- package/import-adapters/gemini-adapter.ts +13 -13
- package/import-adapters/mem0-adapter.ts +10 -10
- package/index.ts +129 -1377
- package/{llm-client.ts → llm/llm-client.ts} +2 -2
- package/{hot-cache-wrapper.ts → memory/hot-cache-wrapper.ts} +5 -5
- package/{memory-runtime.ts → memory/memory-runtime.ts} +1 -1
- package/{pin.ts → memory/pin.ts} +20 -20
- package/{retype-setscope.ts → memory/retype-setscope.ts} +21 -21
- package/{tool-gating.ts → memory/tool-gating.ts} +1 -1
- package/package.json +6 -6
- package/{credential-provider.ts → pairing/credential-provider.ts} +2 -2
- package/{first-run.ts → pairing/first-run.ts} +1 -1
- package/{onboarding-cli.ts → pairing/onboarding-cli.ts} +20 -20
- package/{pair-cli-relay.ts → pairing/pair-cli-relay.ts} +10 -10
- package/{pair-crypto.ts → pairing/pair-crypto.ts} +1 -1
- package/{pair-http.ts → pairing/pair-http.ts} +3 -3
- package/{pair-remote-client.ts → pairing/pair-remote-client.ts} +10 -10
- package/{pair-session-store.ts → pairing/pair-session-store.ts} +4 -4
- package/runtime/config-schema.ts +64 -0
- package/runtime/format-helpers.ts +293 -0
- package/runtime/types.ts +118 -0
- package/skill.json +44 -10
- package/{confirm-indexed.ts → subgraph/confirm-indexed.ts} +1 -1
- package/{subgraph-search.ts → subgraph/subgraph-search.ts} +14 -14
- package/{subgraph-store.ts → subgraph/subgraph-store.ts} +10 -10
- package/dist/consolidation.js +0 -249
- /package/{api-client.ts → billing/api-client.ts} +0 -0
- /package/{gateway-url.ts → billing/gateway-url.ts} +0 -0
- /package/{inbound-user-tracker.ts → billing/inbound-user-tracker.ts} +0 -0
- /package/{relay.ts → billing/relay.ts} +0 -0
- /package/{crypto.ts → crypto/crypto.ts} +0 -0
- /package/{vault-crypto.ts → crypto/vault-crypto.ts} +0 -0
- /package/dist/{api-client.js → billing/api-client.js} +0 -0
- /package/dist/{gateway-url.js → billing/gateway-url.js} +0 -0
- /package/dist/{inbound-user-tracker.js → billing/inbound-user-tracker.js} +0 -0
- /package/dist/{relay.js → billing/relay.js} +0 -0
- /package/dist/{crypto.js → crypto/crypto.js} +0 -0
- /package/dist/{vault-crypto.js → crypto/vault-crypto.js} +0 -0
- /package/dist/{embedder-cache.js → embedding/embedder-cache.js} +0 -0
- /package/dist/{embedder-loader.js → embedding/embedder-loader.js} +0 -0
- /package/dist/{embedder-network.js → embedding/embedder-network.js} +0 -0
- /package/dist/{embedding.js → embedding/embedding.js} +0 -0
- /package/dist/{lsh.js → embedding/lsh.js} +0 -0
- /package/dist/{import-state-manager.js → import/import-state-manager.js} +0 -0
- /package/dist/{llm-profile-reader.js → llm/llm-profile-reader.js} +0 -0
- /package/dist/{native-memory.js → memory/native-memory.js} +0 -0
- /package/dist/{tool-gating.js → memory/tool-gating.js} +0 -0
- /package/dist/{tools.js → memory/tools.js} +0 -0
- /package/dist/{generate-mnemonic.js → pairing/generate-mnemonic.js} +0 -0
- /package/dist/{pair-cli.js → pairing/pair-cli.js} +0 -0
- /package/dist/{pair-page.js → pairing/pair-page.js} +0 -0
- /package/dist/{pair-qr.js → pairing/pair-qr.js} +0 -0
- /package/dist/{restart-auth.js → pairing/restart-auth.js} +0 -0
- /package/dist/{qa-bug-report.js → setup/qa-bug-report.js} +0 -0
- /package/dist/{skill-register.js → setup/skill-register.js} +0 -0
- /package/dist/{trajectory-poller.js → subgraph/trajectory-poller.js} +0 -0
- /package/{embedder-cache.ts → embedding/embedder-cache.ts} +0 -0
- /package/{embedder-loader.ts → embedding/embedder-loader.ts} +0 -0
- /package/{embedder-network.ts → embedding/embedder-network.ts} +0 -0
- /package/{embedding.ts → embedding/embedding.ts} +0 -0
- /package/{lsh.ts → embedding/lsh.ts} +0 -0
- /package/{import-state-manager.ts → import/import-state-manager.ts} +0 -0
- /package/{llm-profile-reader.ts → llm/llm-profile-reader.ts} +0 -0
- /package/{native-memory.ts → memory/native-memory.ts} +0 -0
- /package/{tools.ts → memory/tools.ts} +0 -0
- /package/{generate-mnemonic.ts → pairing/generate-mnemonic.ts} +0 -0
- /package/{pair-cli.ts → pairing/pair-cli.ts} +0 -0
- /package/{pair-page.ts → pairing/pair-page.ts} +0 -0
- /package/{pair-qr.ts → pairing/pair-qr.ts} +0 -0
- /package/{restart-auth.ts → pairing/restart-auth.ts} +0 -0
- /package/{qa-bug-report.ts → setup/qa-bug-report.ts} +0 -0
- /package/{skill-register.ts → setup/skill-register.ts} +0 -0
- /package/{trajectory-poller.ts → subgraph/trajectory-poller.ts} +0 -0
package/index.ts
CHANGED
|
@@ -74,8 +74,8 @@ import {
|
|
|
74
74
|
decrypt,
|
|
75
75
|
generateBlindIndices,
|
|
76
76
|
generateContentFingerprint,
|
|
77
|
-
} from './crypto.js';
|
|
78
|
-
import { createApiClient, type StoreFactPayload } from './api-client.js';
|
|
77
|
+
} from './crypto/crypto.js';
|
|
78
|
+
import { createApiClient, type StoreFactPayload } from './billing/api-client.js';
|
|
79
79
|
import {
|
|
80
80
|
extractFacts,
|
|
81
81
|
extractCrystal,
|
|
@@ -92,7 +92,7 @@ import {
|
|
|
92
92
|
type MemoryType,
|
|
93
93
|
type MemorySource,
|
|
94
94
|
type MemoryScope,
|
|
95
|
-
} from './extractor.js';
|
|
95
|
+
} from './extraction/extractor.js';
|
|
96
96
|
import {
|
|
97
97
|
initLLMClient,
|
|
98
98
|
resolveLLMConfig,
|
|
@@ -102,16 +102,16 @@ import {
|
|
|
102
102
|
getEmbeddingModelId,
|
|
103
103
|
configureEmbedder,
|
|
104
104
|
prefetchEmbedderBundle,
|
|
105
|
-
} from './llm-client.js';
|
|
105
|
+
} from './llm/llm-client.js';
|
|
106
106
|
import {
|
|
107
107
|
defaultAuthProfilesRoot,
|
|
108
108
|
readAllProfileKeys,
|
|
109
109
|
dedupeByProvider,
|
|
110
|
-
} from './llm-profile-reader.js';
|
|
111
|
-
import { LSHHasher } from './lsh.js';
|
|
112
|
-
import { rerank, cosineSimilarity, detectQueryIntent, INTENT_WEIGHTS, type RerankerCandidate } from './reranker.js';
|
|
113
|
-
import { deduplicateBatch } from './semantic-dedup.js';
|
|
114
|
-
import { startTrajectoryPoller, type ExtractedFactLike } from './trajectory-poller.js';
|
|
110
|
+
} from './llm/llm-profile-reader.js';
|
|
111
|
+
import { LSHHasher } from './embedding/lsh.js';
|
|
112
|
+
import { rerank, cosineSimilarity, detectQueryIntent, INTENT_WEIGHTS, type RerankerCandidate } from './embedding/reranker.js';
|
|
113
|
+
import { deduplicateBatch } from './extraction/semantic-dedup.js';
|
|
114
|
+
import { startTrajectoryPoller, type ExtractedFactLike } from './subgraph/trajectory-poller.js';
|
|
115
115
|
import {
|
|
116
116
|
findNearDuplicate,
|
|
117
117
|
shouldSupersede,
|
|
@@ -120,9 +120,9 @@ import {
|
|
|
120
120
|
getConsolidationThreshold,
|
|
121
121
|
STORE_DEDUP_MAX_CANDIDATES,
|
|
122
122
|
type DecryptedCandidate,
|
|
123
|
-
} from './consolidation.js';
|
|
124
|
-
import { isSubgraphMode, getSubgraphConfig, encodeFactProtobuf, submitFactOnChain, submitFactBatchOnChain, deriveSmartAccountAddress, PROTOBUF_VERSION_V4, type FactPayload } from './subgraph-store.js';
|
|
125
|
-
import { confirmIndexed } from './confirm-indexed.js';
|
|
123
|
+
} from './extraction/consolidation.js';
|
|
124
|
+
import { isSubgraphMode, getSubgraphConfig, encodeFactProtobuf, submitFactOnChain, submitFactBatchOnChain, deriveSmartAccountAddress, PROTOBUF_VERSION_V4, type FactPayload } from './subgraph/subgraph-store.js';
|
|
125
|
+
import { confirmIndexed } from './subgraph/confirm-indexed.js';
|
|
126
126
|
import {
|
|
127
127
|
DIGEST_TRAPDOOR,
|
|
128
128
|
buildCanonicalClaim,
|
|
@@ -133,7 +133,7 @@ import {
|
|
|
133
133
|
readClaimFromBlob,
|
|
134
134
|
resolveDigestMode,
|
|
135
135
|
type DigestMode,
|
|
136
|
-
} from './claims-helper.js';
|
|
136
|
+
} from './extraction/claims-helper.js';
|
|
137
137
|
import {
|
|
138
138
|
maybeInjectDigest,
|
|
139
139
|
recompileDigest,
|
|
@@ -141,31 +141,31 @@ import {
|
|
|
141
141
|
isRecompileInProgress,
|
|
142
142
|
tryBeginRecompile,
|
|
143
143
|
endRecompile,
|
|
144
|
-
} from './digest-sync.js';
|
|
144
|
+
} from './digest/digest-sync.js';
|
|
145
145
|
import {
|
|
146
146
|
detectAndResolveContradictions,
|
|
147
147
|
runWeightTuningLoop,
|
|
148
148
|
type ResolutionDecision as ContradictionDecision,
|
|
149
|
-
} from './contradiction-sync.js';
|
|
150
|
-
import { searchSubgraph, searchSubgraphBroadened, getSubgraphFactCount, fetchFactById } from './subgraph-search.js';
|
|
149
|
+
} from './contradiction/contradiction-sync.js';
|
|
150
|
+
import { searchSubgraph, searchSubgraphBroadened, getSubgraphFactCount, fetchFactById } from './subgraph/subgraph-search.js';
|
|
151
151
|
import {
|
|
152
152
|
executePinOperation,
|
|
153
153
|
validatePinArgs,
|
|
154
154
|
type PinOpDeps,
|
|
155
|
-
} from './pin.js';
|
|
155
|
+
} from './memory/pin.js';
|
|
156
156
|
import {
|
|
157
157
|
runNonInteractiveOnboard,
|
|
158
158
|
type NonInteractiveOnboardResult,
|
|
159
|
-
} from './onboarding-cli.js';
|
|
160
|
-
import { PluginHotCache, type HotFact } from './hot-cache-wrapper.js';
|
|
159
|
+
} from './pairing/onboarding-cli.js';
|
|
160
|
+
import { PluginHotCache, type HotFact } from './memory/hot-cache-wrapper.js';
|
|
161
161
|
import { CONFIG, setRecoveryPhraseOverride } from './config.js';
|
|
162
|
-
import { buildRelayHeaders } from './relay-headers.js';
|
|
162
|
+
import { buildRelayHeaders } from './billing/relay-headers.js';
|
|
163
163
|
import {
|
|
164
164
|
readBillingCache,
|
|
165
165
|
writeBillingCache,
|
|
166
166
|
BILLING_CACHE_PATH,
|
|
167
167
|
type BillingCache,
|
|
168
|
-
} from './billing-cache.js';
|
|
168
|
+
} from './billing/billing-cache.js';
|
|
169
169
|
import {
|
|
170
170
|
ensureMemoryHeaderFile,
|
|
171
171
|
loadCredentialsJson,
|
|
@@ -183,24 +183,45 @@ import {
|
|
|
183
183
|
checkCredentialsFileMode,
|
|
184
184
|
type OnboardingState,
|
|
185
185
|
} from './fs-helpers.js';
|
|
186
|
-
import { isRcBuild } from './qa-bug-report.js';
|
|
187
|
-
import { decideToolGate, isGatedToolName } from './tool-gating.js';
|
|
186
|
+
import { isRcBuild } from './setup/qa-bug-report.js';
|
|
187
|
+
import { decideToolGate, isGatedToolName } from './memory/tool-gating.js';
|
|
188
188
|
import {
|
|
189
189
|
resolveRestartAuth,
|
|
190
190
|
rejectMessageFor,
|
|
191
191
|
type RestartAuthConfig,
|
|
192
|
-
} from './restart-auth.js';
|
|
192
|
+
} from './pairing/restart-auth.js';
|
|
193
193
|
import {
|
|
194
194
|
recordInboundUser,
|
|
195
195
|
getDistinctInboundUserCount,
|
|
196
196
|
resolveTrackerPath,
|
|
197
|
-
} from './inbound-user-tracker.js';
|
|
198
|
-
import { detectFirstRun, buildWelcomePrepend, type GatewayMode } from './first-run.js';
|
|
199
|
-
import { buildPairRoutes } from './pair-http.js';
|
|
200
|
-
import { detectGatewayHost } from './gateway-url.js';
|
|
201
|
-
import { registerNativeMemory, type TrNativeMemoryDeps } from './native-memory.js';
|
|
202
|
-
import { ensureSkillRegistered } from './skill-register.js';
|
|
203
|
-
import type {
|
|
197
|
+
} from './billing/inbound-user-tracker.js';
|
|
198
|
+
import { detectFirstRun, buildWelcomePrepend, type GatewayMode } from './pairing/first-run.js';
|
|
199
|
+
import { buildPairRoutes } from './pairing/pair-http.js';
|
|
200
|
+
import { detectGatewayHost } from './billing/gateway-url.js';
|
|
201
|
+
import { registerNativeMemory, type TrNativeMemoryDeps } from './memory/native-memory.js';
|
|
202
|
+
import { ensureSkillRegistered } from './setup/skill-register.js';
|
|
203
|
+
import type { OpenClawPluginApi, MigrationFact, SmartImportContext } from './runtime/types.js';
|
|
204
|
+
import {
|
|
205
|
+
humanizeError,
|
|
206
|
+
buildPairingUrl,
|
|
207
|
+
resolveGatewayMode,
|
|
208
|
+
computeCandidatePool,
|
|
209
|
+
encryptToHex,
|
|
210
|
+
decryptFromHex,
|
|
211
|
+
textScore,
|
|
212
|
+
relativeTime,
|
|
213
|
+
} from './runtime/format-helpers.js';
|
|
214
|
+
import { CONFIG_SCHEMA } from './runtime/config-schema.js';
|
|
215
|
+
import { filterByImportance } from './extraction/importance-filter.js';
|
|
216
|
+
import {
|
|
217
|
+
handlePluginImportFrom,
|
|
218
|
+
handleImportStatus,
|
|
219
|
+
handleImportAbort,
|
|
220
|
+
configureImportRuntime,
|
|
221
|
+
isImportInProgress,
|
|
222
|
+
setImportInProgress,
|
|
223
|
+
} from './import/import-runtime.js';
|
|
224
|
+
import type { TrFact, TrPinnedFact, TrQuotaState } from './memory/memory-runtime.js';
|
|
204
225
|
import { validateMnemonic } from '@scure/bip39';
|
|
205
226
|
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
206
227
|
import crypto from 'node:crypto';
|
|
@@ -213,7 +234,7 @@ import {
|
|
|
213
234
|
isImportStale,
|
|
214
235
|
readMostRecentActiveImport,
|
|
215
236
|
type ImportState,
|
|
216
|
-
} from './import-state-manager.js';
|
|
237
|
+
} from './import/import-state-manager.js';
|
|
217
238
|
|
|
218
239
|
// CJS-style require for the @totalreclaw/core WASM module. We keep this
|
|
219
240
|
// load path lazy (only inside getSmartImportWasm() below) so a partial
|
|
@@ -226,117 +247,9 @@ import {
|
|
|
226
247
|
const __cjsRequire = createRequire(import.meta.url);
|
|
227
248
|
|
|
228
249
|
// ---------------------------------------------------------------------------
|
|
229
|
-
// OpenClaw Plugin API type
|
|
250
|
+
// OpenClaw Plugin API type — extracted to ./runtime/types.ts (imported above).
|
|
230
251
|
// ---------------------------------------------------------------------------
|
|
231
252
|
|
|
232
|
-
interface OpenClawPluginApi {
|
|
233
|
-
logger: {
|
|
234
|
-
info(...args: unknown[]): void;
|
|
235
|
-
warn(...args: unknown[]): void;
|
|
236
|
-
error(...args: unknown[]): void;
|
|
237
|
-
};
|
|
238
|
-
config?: {
|
|
239
|
-
agents?: {
|
|
240
|
-
defaults?: {
|
|
241
|
-
model?: {
|
|
242
|
-
primary?: string;
|
|
243
|
-
};
|
|
244
|
-
};
|
|
245
|
-
};
|
|
246
|
-
models?: {
|
|
247
|
-
providers?: Record<string, {
|
|
248
|
-
baseUrl: string;
|
|
249
|
-
apiKey?: string;
|
|
250
|
-
api?: string;
|
|
251
|
-
models?: Array<{ id: string; [k: string]: unknown }>;
|
|
252
|
-
[k: string]: unknown;
|
|
253
|
-
}>;
|
|
254
|
-
[k: string]: unknown;
|
|
255
|
-
};
|
|
256
|
-
[key: string]: unknown;
|
|
257
|
-
};
|
|
258
|
-
pluginConfig?: Record<string, unknown>;
|
|
259
|
-
registerTool(tool: unknown, opts?: { name?: string; names?: string[] }): void;
|
|
260
|
-
registerService(service: { id: string; start(): void; stop?(): void }): void;
|
|
261
|
-
on(hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }): void;
|
|
262
|
-
/**
|
|
263
|
-
* 3.2.0 — register a top-level `openclaw <cmd>` subcommand. The handler
|
|
264
|
-
* receives a commander `Command` to attach subcommands to. Output goes
|
|
265
|
-
* straight to the user's TTY; nothing touches the LLM or the transcript.
|
|
266
|
-
* We deliberately type `program` as `unknown` at this boundary because
|
|
267
|
-
* we don't import the SDK's full types; the runtime shape is commander's
|
|
268
|
-
* `Command` which we cast at the call site.
|
|
269
|
-
*/
|
|
270
|
-
registerCli?(
|
|
271
|
-
registrar: (ctx: { program: unknown; config?: unknown; workspaceDir?: string; logger?: unknown }) => void | Promise<void>,
|
|
272
|
-
opts?: { commands?: string[] },
|
|
273
|
-
): void;
|
|
274
|
-
/**
|
|
275
|
-
* 3.2.0 — register a slash command (e.g. `/totalreclaw`). The handler
|
|
276
|
-
* runs before the agent; its reply is delivered via the channel adapter.
|
|
277
|
-
* Reply text IS appended to the session transcript (see gateway-cli
|
|
278
|
-
* L9300-9312), so we only emit non-secret pointers.
|
|
279
|
-
*/
|
|
280
|
-
registerCommand?(command: {
|
|
281
|
-
name: string;
|
|
282
|
-
description: string;
|
|
283
|
-
acceptsArgs?: boolean;
|
|
284
|
-
requireAuth?: boolean;
|
|
285
|
-
handler: (ctx: {
|
|
286
|
-
senderId?: string;
|
|
287
|
-
channel?: string;
|
|
288
|
-
args?: string;
|
|
289
|
-
commandBody?: string;
|
|
290
|
-
isAuthorizedSender?: boolean;
|
|
291
|
-
config?: unknown;
|
|
292
|
-
}) => { text: string } | Promise<{ text: string }>;
|
|
293
|
-
}): void;
|
|
294
|
-
/**
|
|
295
|
-
* 3.3.0 — register an HTTP route on the gateway's HTTP server.
|
|
296
|
-
* Used by the QR-pairing flow to serve the pairing page + the
|
|
297
|
-
* encrypted-payload respond endpoint. Path is exact-match against
|
|
298
|
-
* `new URL(req.url, ...).pathname`; no params supported.
|
|
299
|
-
*/
|
|
300
|
-
registerHttpRoute?(params: {
|
|
301
|
-
path: string;
|
|
302
|
-
handler: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void> | void;
|
|
303
|
-
/** OpenClaw 2026.4.2+ — required; loader silently drops the route if absent. */
|
|
304
|
-
auth: 'gateway' | 'plugin';
|
|
305
|
-
}): void;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
// Human-friendly error messages
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Translate technical error messages from the on-chain submission pipeline
|
|
314
|
-
* into user-friendly messages. The original technical details are still
|
|
315
|
-
* logged via api.logger — this only affects what the agent sees.
|
|
316
|
-
*/
|
|
317
|
-
function humanizeError(rawMessage: string): string {
|
|
318
|
-
if (rawMessage.includes('AA23')) {
|
|
319
|
-
return 'Memory storage temporarily unavailable. Will retry next time.';
|
|
320
|
-
}
|
|
321
|
-
if (rawMessage.includes('AA10')) {
|
|
322
|
-
return 'Please wait a moment before storing more memories.';
|
|
323
|
-
}
|
|
324
|
-
if (rawMessage.includes('AA25')) {
|
|
325
|
-
return 'Memory storage busy. Will retry.';
|
|
326
|
-
}
|
|
327
|
-
if (rawMessage.includes('pm_sponsorUserOperation')) {
|
|
328
|
-
return 'Memory storage service temporarily unavailable.';
|
|
329
|
-
}
|
|
330
|
-
if (/Relay returned HTTP\s*404/.test(rawMessage)) {
|
|
331
|
-
return 'Memory service is temporarily offline.';
|
|
332
|
-
}
|
|
333
|
-
if (/Relay returned HTTP\s*5\d\d/.test(rawMessage)) {
|
|
334
|
-
return 'Memory service encountered a temporary error. Will retry next time.';
|
|
335
|
-
}
|
|
336
|
-
// Pass through non-technical messages as-is.
|
|
337
|
-
return rawMessage;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
253
|
// ---------------------------------------------------------------------------
|
|
341
254
|
// Persistent credential storage
|
|
342
255
|
// ---------------------------------------------------------------------------
|
|
@@ -348,177 +261,7 @@ const CREDENTIALS_PATH = CONFIG.credentialsPath;
|
|
|
348
261
|
// 3.3.0 — pairing URL resolution
|
|
349
262
|
// ---------------------------------------------------------------------------
|
|
350
263
|
|
|
351
|
-
|
|
352
|
-
* Build the full pairing URL (including `#pk=` fragment) for a fresh
|
|
353
|
-
* pairing session. Pulls gateway config from `api.config.gateway`.
|
|
354
|
-
*
|
|
355
|
-
* Resolution order (3.3.1 — six-layer cascade):
|
|
356
|
-
* 1. `plugins.entries.totalreclaw.config.publicUrl` — explicit override
|
|
357
|
-
* 2. `gateway.remote.url` — OpenClaw's own remote-gateway URL
|
|
358
|
-
* 3. `gateway.bind === 'custom'` + `gateway.customBindHost` + port
|
|
359
|
-
* 4. Tailscale auto-detect — `tailscale status --json` → `https://<MagicDNS>`
|
|
360
|
-
* (assumes `tailscale serve` proxies to the gateway port on 443)
|
|
361
|
-
* 5. LAN auto-detect — first non-loopback, non-virtual IPv4 interface.
|
|
362
|
-
* Emits a warning: "only works on the same network".
|
|
363
|
-
* 6. Fallback `http://localhost:<port>` — warns with a pointer to
|
|
364
|
-
* configure `plugins.entries.totalreclaw.config.publicUrl`.
|
|
365
|
-
*
|
|
366
|
-
* Always returns a working URL string; never throws. The caller (CLI or
|
|
367
|
-
* JSON output) prints whatever we give it.
|
|
368
|
-
*/
|
|
369
|
-
function buildPairingUrl(
|
|
370
|
-
api: Pick<OpenClawPluginApi, 'config' | 'pluginConfig' | 'logger'>,
|
|
371
|
-
session: { sid: string; pkGatewayB64: string },
|
|
372
|
-
): string {
|
|
373
|
-
const cfg = api.config as {
|
|
374
|
-
gateway?: {
|
|
375
|
-
port?: number;
|
|
376
|
-
bind?: string;
|
|
377
|
-
customBindHost?: string;
|
|
378
|
-
tls?: { enabled?: boolean };
|
|
379
|
-
remote?: { url?: string };
|
|
380
|
-
};
|
|
381
|
-
} | undefined;
|
|
382
|
-
const pluginCfg = (api.pluginConfig ?? {}) as { publicUrl?: string };
|
|
383
|
-
|
|
384
|
-
const tlsEnabled = cfg?.gateway?.tls?.enabled === true;
|
|
385
|
-
const scheme = tlsEnabled ? 'https' : 'http';
|
|
386
|
-
const port = cfg?.gateway?.port ?? 18789;
|
|
387
|
-
|
|
388
|
-
let base: string;
|
|
389
|
-
|
|
390
|
-
// Layer 1 — explicit user override
|
|
391
|
-
if (typeof pluginCfg.publicUrl === 'string' && pluginCfg.publicUrl.trim()) {
|
|
392
|
-
base = pluginCfg.publicUrl.replace(/\/+$/, '');
|
|
393
|
-
base = base.replace(/^wss:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://');
|
|
394
|
-
}
|
|
395
|
-
// Layer 2 — OpenClaw gateway remote URL
|
|
396
|
-
else if (typeof cfg?.gateway?.remote?.url === 'string' && cfg.gateway.remote.url.trim()) {
|
|
397
|
-
base = cfg.gateway.remote.url.trim().replace(/\/+$/, '');
|
|
398
|
-
base = base.replace(/^wss:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://');
|
|
399
|
-
}
|
|
400
|
-
// Layer 3 — gateway.bind = custom + explicit customBindHost
|
|
401
|
-
else if (cfg?.gateway?.bind === 'custom' && cfg.gateway.customBindHost) {
|
|
402
|
-
base = `${scheme}://${cfg.gateway.customBindHost}:${port}`;
|
|
403
|
-
}
|
|
404
|
-
// Layers 4 + 5 — auto-detect via gateway-url helper (Tailscale CGNAT, then LAN)
|
|
405
|
-
else {
|
|
406
|
-
let detected: ReturnType<typeof detectGatewayHost> = null;
|
|
407
|
-
// issue #110 fix 4 — pass `isDocker` so LAN detection skips
|
|
408
|
-
// 172.16/12 bridge IPs that no external browser can reach.
|
|
409
|
-
let isDocker = false;
|
|
410
|
-
try {
|
|
411
|
-
isDocker = isRunningInDocker();
|
|
412
|
-
} catch {
|
|
413
|
-
// Defensive: never block URL building on Docker sniff errors.
|
|
414
|
-
isDocker = false;
|
|
415
|
-
}
|
|
416
|
-
try {
|
|
417
|
-
detected = detectGatewayHost({ isDocker });
|
|
418
|
-
} catch (err) {
|
|
419
|
-
api.logger.warn(
|
|
420
|
-
`TotalReclaw: host autodetect crashed: ${err instanceof Error ? err.message : String(err)} — falling back to localhost`,
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
if (detected?.kind === 'tailscale') {
|
|
424
|
-
// 3.3.1-rc.2: we surface the raw Tailscale CGNAT IP because passive
|
|
425
|
-
// NIC detection (no subprocess) cannot resolve the MagicDNS name.
|
|
426
|
-
// Caller can override via `publicUrl` for a proper https://<magicdns>.
|
|
427
|
-
// The IP + port URL still works inside the tailnet (peers can reach
|
|
428
|
-
// each other by CGNAT IP directly). TLS defaults to the gateway's
|
|
429
|
-
// own config because we no longer assume `tailscale serve`.
|
|
430
|
-
base = `${scheme}://${detected.host}:${port}`;
|
|
431
|
-
api.logger.warn(
|
|
432
|
-
`TotalReclaw: pairing URL using Tailscale CGNAT IP ${detected.host}:${port} — ` +
|
|
433
|
-
detected.note,
|
|
434
|
-
);
|
|
435
|
-
} else if (detected?.kind === 'lan') {
|
|
436
|
-
base = `${scheme}://${detected.host}:${port}`;
|
|
437
|
-
api.logger.warn(
|
|
438
|
-
`TotalReclaw: pairing URL using LAN host ${detected.host}:${port} — ` +
|
|
439
|
-
`this URL only works from the same network. ` +
|
|
440
|
-
`Set plugins.entries.totalreclaw.config.publicUrl for remote access.`,
|
|
441
|
-
);
|
|
442
|
-
} else {
|
|
443
|
-
// Layer 6 — localhost fallback (or Docker-aware relay-pointer warning)
|
|
444
|
-
const bind = cfg?.gateway?.bind;
|
|
445
|
-
if (isDocker) {
|
|
446
|
-
// issue #110 fix 4: inside Docker the LAN IP is container-internal
|
|
447
|
-
// and useless. Loopback localhost only works for `docker exec`
|
|
448
|
-
// tests. The CORRECT pair URL for Docker is the relay-brokered
|
|
449
|
-
// path served by `tr pair` / the `/plugin/totalreclaw/pair/*` HTTP
|
|
450
|
-
// routes (CONFIG.pairMode === 'relay' since rc.11). The CLI-only
|
|
451
|
-
// path here cannot mint a relay session synchronously (the relay
|
|
452
|
-
// handshake needs a WS round-trip), so we emit the loopback URL
|
|
453
|
-
// with a LOUD warning pointing the operator at the pair CLI /
|
|
454
|
-
// publicUrl override.
|
|
455
|
-
api.logger.warn(
|
|
456
|
-
`TotalReclaw: Docker container detected — pairing URL falling back to ` +
|
|
457
|
-
`http://localhost:${port}, which is unreachable from the host browser. ` +
|
|
458
|
-
`Run \`tr pair --url-pin\` (or \`openclaw totalreclaw pair generate --url-pin-only\`) ` +
|
|
459
|
-
`on the gateway host to mint a relay-brokered pair URL that reaches the host browser, ` +
|
|
460
|
-
`OR set plugins.entries.totalreclaw.config.publicUrl ` +
|
|
461
|
-
`to your gateway's host-reachable URL (e.g. http://<host-ip>:${port} when the ` +
|
|
462
|
-
`Docker port is published). Setting TOTALRECLAW_PAIR_MODE=relay is the default; ` +
|
|
463
|
-
`air-gapped operators on TOTALRECLAW_PAIR_MODE=local must publish a port + set publicUrl.`,
|
|
464
|
-
);
|
|
465
|
-
} else if (bind === 'lan' || bind === 'tailnet') {
|
|
466
|
-
api.logger.warn(
|
|
467
|
-
`TotalReclaw: pairing URL falling back to localhost because gateway.bind=${bind} could not be autodetected. ` +
|
|
468
|
-
'Set plugins.entries.totalreclaw.config.publicUrl to override.',
|
|
469
|
-
);
|
|
470
|
-
} else {
|
|
471
|
-
api.logger.warn(
|
|
472
|
-
`TotalReclaw: pairing URL fell back to http://localhost:${port} — this URL only works on this machine. ` +
|
|
473
|
-
`Configure plugins.entries.totalreclaw.config.publicUrl for remote access.`,
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
base = `${scheme}://localhost:${port}`;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return `${base}/plugin/totalreclaw/pair/finish?sid=${encodeURIComponent(session.sid)}#pk=${encodeURIComponent(session.pkGatewayB64)}`;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Resolve whether this plugin is running on a `local` or `remote` gateway.
|
|
485
|
-
*
|
|
486
|
-
* Follows the same config surface `buildPairingUrl` uses:
|
|
487
|
-
* - `pluginConfig.publicUrl` set + non-localhost → remote
|
|
488
|
-
* - `gateway.remote.url` set + non-localhost → remote
|
|
489
|
-
* - `gateway.bind === 'lan' | 'tailnet' | 'custom'` → remote
|
|
490
|
-
* - anything else → local
|
|
491
|
-
*
|
|
492
|
-
* We treat a `publicUrl` or `remote.url` that points at `localhost` /
|
|
493
|
-
* `127.*` as local because that is what a dev-loopback override looks like;
|
|
494
|
-
* no one publishes a remote QR pairing for localhost.
|
|
495
|
-
*/
|
|
496
|
-
function resolveGatewayMode(
|
|
497
|
-
api: Pick<OpenClawPluginApi, 'config' | 'pluginConfig'>,
|
|
498
|
-
): GatewayMode {
|
|
499
|
-
const cfg = api.config as
|
|
500
|
-
| { gateway?: { bind?: string; remote?: { url?: string } } }
|
|
501
|
-
| undefined;
|
|
502
|
-
const pluginCfg = (api.pluginConfig ?? {}) as { publicUrl?: string };
|
|
503
|
-
const looksLocal = (url: string | undefined): boolean => {
|
|
504
|
-
if (!url) return true;
|
|
505
|
-
const u = url.trim().toLowerCase();
|
|
506
|
-
if (u === '') return true;
|
|
507
|
-
return /^(?:wss?:\/\/|https?:\/\/)?(?:localhost|127\.|0\.0\.0\.0)/.test(u);
|
|
508
|
-
};
|
|
509
|
-
if (typeof pluginCfg.publicUrl === 'string' && !looksLocal(pluginCfg.publicUrl)) {
|
|
510
|
-
return 'remote';
|
|
511
|
-
}
|
|
512
|
-
const remoteUrl = cfg?.gateway?.remote?.url;
|
|
513
|
-
if (typeof remoteUrl === 'string' && !looksLocal(remoteUrl)) {
|
|
514
|
-
return 'remote';
|
|
515
|
-
}
|
|
516
|
-
const bind = cfg?.gateway?.bind;
|
|
517
|
-
if (bind === 'lan' || bind === 'tailnet' || bind === 'custom') {
|
|
518
|
-
return 'remote';
|
|
519
|
-
}
|
|
520
|
-
return 'local';
|
|
521
|
-
}
|
|
264
|
+
// buildPairingUrl / resolveGatewayMode extracted to ./runtime/format-helpers.ts.
|
|
522
265
|
|
|
523
266
|
// ---------------------------------------------------------------------------
|
|
524
267
|
// Cosine similarity threshold — skip injection when top result is below this
|
|
@@ -564,9 +307,6 @@ const SEMANTIC_SKIP_THRESHOLD = CONFIG.semanticSkipThreshold;
|
|
|
564
307
|
// Auto-extract throttle (C3): only extract every N turns in agent_end hook
|
|
565
308
|
let turnsSinceLastExtraction = 0;
|
|
566
309
|
|
|
567
|
-
// BUG-2 fix: Skip agent_end extraction during import operations.
|
|
568
|
-
// Import failures previously triggered agent_end → re-extraction → re-import loops.
|
|
569
|
-
let _importInProgress = false;
|
|
570
310
|
const AUTO_EXTRACT_EVERY_TURNS_ENV = CONFIG.extractInterval;
|
|
571
311
|
|
|
572
312
|
// Hard cap on facts per extraction to prevent LLM over-extraction from dense conversations
|
|
@@ -720,23 +460,7 @@ let lastFactCountFetch: number = 0;
|
|
|
720
460
|
/** Cache TTL for fact count: 5 minutes. */
|
|
721
461
|
const FACT_COUNT_CACHE_TTL = 5 * 60 * 1000;
|
|
722
462
|
|
|
723
|
-
|
|
724
|
-
* Compute the candidate pool size from a fact count.
|
|
725
|
-
*
|
|
726
|
-
* Server-side config takes priority (from billing cache), then local fallback.
|
|
727
|
-
* The server computes the optimal pool based on vault size and tier caps.
|
|
728
|
-
*
|
|
729
|
-
* Local fallback formula: pool = min(max(factCount * 3, 400), 5000)
|
|
730
|
-
* - At least 400 candidates (even for tiny vaults)
|
|
731
|
-
* - At most 5000 candidates (to bound decryption + reranking cost)
|
|
732
|
-
* - 3x fact count in between
|
|
733
|
-
*/
|
|
734
|
-
function computeCandidatePool(factCount: number): number {
|
|
735
|
-
const cache = readBillingCache();
|
|
736
|
-
if (cache?.features?.max_candidate_pool != null) return cache.features.max_candidate_pool;
|
|
737
|
-
// Fallback to local formula if no server config
|
|
738
|
-
return Math.min(Math.max(factCount * 3, 400), 5000);
|
|
739
|
-
}
|
|
463
|
+
// computeCandidatePool extracted to ./runtime/format-helpers.ts.
|
|
740
464
|
|
|
741
465
|
/**
|
|
742
466
|
* Fetch the user's fact count from the server, with caching.
|
|
@@ -1051,10 +775,6 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
|
|
|
1051
775
|
}
|
|
1052
776
|
}
|
|
1053
777
|
|
|
1054
|
-
function isDocker(): boolean {
|
|
1055
|
-
return isRunningInDocker();
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
778
|
function buildSetupErrorMsg(): string {
|
|
1059
779
|
// NOTE: the legacy `totalreclaw_setup` agent tool was retired in 3.2.0
|
|
1060
780
|
// (phrase-safety: the agent must never accept or relay a recovery phrase).
|
|
@@ -1068,33 +788,6 @@ function buildSetupErrorMsg(): string {
|
|
|
1068
788
|
'Do NOT ask the user for a recovery phrase and do NOT attempt to generate or relay one yourself.';
|
|
1069
789
|
}
|
|
1070
790
|
|
|
1071
|
-
function buildSetupErrorMsgLegacy(): string {
|
|
1072
|
-
const base =
|
|
1073
|
-
'TotalReclaw setup required:\n' +
|
|
1074
|
-
'1. Set TOTALRECLAW_RECOVERY_PHRASE — ask the user if they have an existing recovery phrase or generate a new 12-word recovery phrase.\n' +
|
|
1075
|
-
'2. Restart the gateway to apply changes.\n' +
|
|
1076
|
-
' (Optional: set TOTALRECLAW_SELF_HOSTED=true if using your own server instead of the managed service.)\n\n';
|
|
1077
|
-
|
|
1078
|
-
if (isDocker()) {
|
|
1079
|
-
return base +
|
|
1080
|
-
'Running in Docker — pass env vars via `-e` flags or your compose file:\n' +
|
|
1081
|
-
' -e TOTALRECLAW_RECOVERY_PHRASE="word1 word2 ..."';
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
if (process.platform === 'darwin') {
|
|
1085
|
-
return base +
|
|
1086
|
-
'Running on macOS — add env vars to the LaunchAgent plist at\n' +
|
|
1087
|
-
'~/Library/LaunchAgents/ai.openclaw.gateway.plist under <key>EnvironmentVariables</key>:\n' +
|
|
1088
|
-
' <key>TOTALRECLAW_RECOVERY_PHRASE</key><string>word1 word2 ...</string>\n' +
|
|
1089
|
-
'Then run: openclaw gateway restart';
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
return base +
|
|
1093
|
-
'Running on Linux — add env vars to the systemd unit override or your shell profile:\n' +
|
|
1094
|
-
' export TOTALRECLAW_RECOVERY_PHRASE="word1 word2 ..."\n' +
|
|
1095
|
-
'Then run: openclaw gateway restart';
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
791
|
const SETUP_ERROR_MSG = buildSetupErrorMsg();
|
|
1099
792
|
|
|
1100
793
|
/**
|
|
@@ -1374,10 +1067,10 @@ async function searchForNearDuplicates(
|
|
|
1374
1067
|
|
|
1375
1068
|
if (decryptedCandidates.length === 0) return null;
|
|
1376
1069
|
|
|
1377
|
-
const
|
|
1378
|
-
if (!
|
|
1070
|
+
const nearDup = findNearDuplicate(factEmbedding, decryptedCandidates, getStoreDedupThreshold());
|
|
1071
|
+
if (!nearDup) return null;
|
|
1379
1072
|
|
|
1380
|
-
return { match:
|
|
1073
|
+
return { match: nearDup.existingFact, similarity: nearDup.similarity };
|
|
1381
1074
|
} catch (err) {
|
|
1382
1075
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1383
1076
|
logger.warn(`Store-time dedup search failed (proceeding with store): ${msg}`);
|
|
@@ -1389,16 +1082,7 @@ async function searchForNearDuplicates(
|
|
|
1389
1082
|
// Utility helpers
|
|
1390
1083
|
// ---------------------------------------------------------------------------
|
|
1391
1084
|
|
|
1392
|
-
|
|
1393
|
-
* Encrypt a plaintext document string and return its hex-encoded ciphertext.
|
|
1394
|
-
*
|
|
1395
|
-
* The server stores blobs as hex (not base64), so we convert the base64
|
|
1396
|
-
* output of `encrypt()` into hex.
|
|
1397
|
-
*/
|
|
1398
|
-
function encryptToHex(plaintext: string, key: Buffer): string {
|
|
1399
|
-
const b64 = encrypt(plaintext, key);
|
|
1400
|
-
return Buffer.from(b64, 'base64').toString('hex');
|
|
1401
|
-
}
|
|
1085
|
+
// encryptToHex extracted to ./runtime/format-helpers.ts.
|
|
1402
1086
|
|
|
1403
1087
|
// Plugin v3.0.0 removed the legacy claim-format fallback. Write path
|
|
1404
1088
|
// always emits Memory Taxonomy v1 JSON blobs. The logClaimFormatOnce
|
|
@@ -1512,8 +1196,8 @@ function scheduleDigestRecompile(
|
|
|
1512
1196
|
version: PROTOBUF_VERSION_V4,
|
|
1513
1197
|
});
|
|
1514
1198
|
const config = { ...getSubgraphConfig(), authKeyHex: authKey, walletAddress: ownerForBatch };
|
|
1515
|
-
const
|
|
1516
|
-
if (!
|
|
1199
|
+
const batchResult = await submitFactBatchOnChain([protobuf], config);
|
|
1200
|
+
if (!batchResult.success) {
|
|
1517
1201
|
throw new Error('Digest store UserOp did not succeed on-chain');
|
|
1518
1202
|
}
|
|
1519
1203
|
};
|
|
@@ -1539,8 +1223,8 @@ function scheduleDigestRecompile(
|
|
|
1539
1223
|
};
|
|
1540
1224
|
const protobuf = encodeFactProtobuf(tombstone);
|
|
1541
1225
|
const config = { ...getSubgraphConfig(), authKeyHex: authKey, walletAddress: ownerForBatch };
|
|
1542
|
-
const
|
|
1543
|
-
if (!
|
|
1226
|
+
const batchResult = await submitFactBatchOnChain([protobuf], config);
|
|
1227
|
+
if (!batchResult.success) {
|
|
1544
1228
|
throw new Error('Digest tombstone UserOp did not succeed on-chain');
|
|
1545
1229
|
}
|
|
1546
1230
|
};
|
|
@@ -1576,32 +1260,13 @@ function scheduleDigestRecompile(
|
|
|
1576
1260
|
});
|
|
1577
1261
|
}
|
|
1578
1262
|
|
|
1579
|
-
|
|
1580
|
-
* Decrypt a hex-encoded ciphertext blob into a UTF-8 string.
|
|
1581
|
-
*/
|
|
1582
|
-
function decryptFromHex(hexBlob: string, key: Buffer): string {
|
|
1583
|
-
const hex = hexBlob.startsWith('0x') ? hexBlob.slice(2) : hexBlob;
|
|
1584
|
-
const b64 = Buffer.from(hex, 'hex').toString('base64');
|
|
1585
|
-
return decrypt(b64, key);
|
|
1586
|
-
}
|
|
1263
|
+
// decryptFromHex extracted to ./runtime/format-helpers.ts.
|
|
1587
1264
|
|
|
1588
1265
|
// ---------------------------------------------------------------------------
|
|
1589
1266
|
// Migration GraphQL helpers
|
|
1590
1267
|
// ---------------------------------------------------------------------------
|
|
1591
1268
|
|
|
1592
|
-
|
|
1593
|
-
id: string;
|
|
1594
|
-
owner: string;
|
|
1595
|
-
encryptedBlob: string;
|
|
1596
|
-
encryptedEmbedding: string | null;
|
|
1597
|
-
decayScore: string;
|
|
1598
|
-
isActive: boolean;
|
|
1599
|
-
contentFp: string;
|
|
1600
|
-
source: string;
|
|
1601
|
-
agentId: string;
|
|
1602
|
-
version: number;
|
|
1603
|
-
timestamp: string;
|
|
1604
|
-
}
|
|
1269
|
+
// MigrationFact — extracted to ./runtime/types.ts (imported above).
|
|
1605
1270
|
|
|
1606
1271
|
const MIGRATION_PAGE_SIZE = 1000;
|
|
1607
1272
|
|
|
@@ -1649,8 +1314,8 @@ async function fetchAllFactsByOwner(
|
|
|
1649
1314
|
? { owner, first: MIGRATION_PAGE_SIZE, lastId }
|
|
1650
1315
|
: { owner, first: MIGRATION_PAGE_SIZE };
|
|
1651
1316
|
|
|
1652
|
-
const
|
|
1653
|
-
const facts =
|
|
1317
|
+
const subgraphResponse = await migrationGqlQuery<{ facts?: MigrationFact[] }>(subgraphUrl, query, vars, authKey);
|
|
1318
|
+
const facts = subgraphResponse?.facts ?? [];
|
|
1654
1319
|
if (facts.length === 0) break;
|
|
1655
1320
|
allFacts.push(...facts);
|
|
1656
1321
|
if (facts.length < MIGRATION_PAGE_SIZE) break;
|
|
@@ -1678,8 +1343,8 @@ async function fetchContentFingerprintsByOwner(
|
|
|
1678
1343
|
? { owner, first: MIGRATION_PAGE_SIZE, lastId }
|
|
1679
1344
|
: { owner, first: MIGRATION_PAGE_SIZE };
|
|
1680
1345
|
|
|
1681
|
-
const
|
|
1682
|
-
const facts =
|
|
1346
|
+
const subgraphResponse = await migrationGqlQuery<{ facts?: Array<{ id: string; contentFp: string }> }>(subgraphUrl, query, vars, authKey);
|
|
1347
|
+
const facts = subgraphResponse?.facts ?? [];
|
|
1683
1348
|
if (facts.length === 0) break;
|
|
1684
1349
|
for (const f of facts) {
|
|
1685
1350
|
if (f.contentFp) fps.add(f.contentFp);
|
|
@@ -1697,24 +1362,24 @@ async function fetchBlindIndicesByFactIds(
|
|
|
1697
1362
|
factIds: string[],
|
|
1698
1363
|
authKey: string,
|
|
1699
1364
|
): Promise<Map<string, string[]>> {
|
|
1700
|
-
const
|
|
1365
|
+
const hashesByFactId = new Map<string, string[]>();
|
|
1701
1366
|
const CHUNK = 50;
|
|
1702
1367
|
|
|
1703
1368
|
for (let i = 0; i < factIds.length; i += CHUNK) {
|
|
1704
1369
|
const chunk = factIds.slice(i, i + CHUNK);
|
|
1705
1370
|
const query = `query($factIds:[String!]!,$first:Int!){blindIndexes(where:{fact_in:$factIds},first:$first){hash fact{id}}}`;
|
|
1706
|
-
const
|
|
1371
|
+
const subgraphResponse = await migrationGqlQuery<{
|
|
1707
1372
|
blindIndexes?: Array<{ hash: string; fact: { id: string } }>;
|
|
1708
1373
|
}>(subgraphUrl, query, { factIds: chunk, first: 1000 }, authKey);
|
|
1709
1374
|
|
|
1710
|
-
for (const entry of
|
|
1711
|
-
const existing =
|
|
1375
|
+
for (const entry of subgraphResponse?.blindIndexes ?? []) {
|
|
1376
|
+
const existing = hashesByFactId.get(entry.fact.id) || [];
|
|
1712
1377
|
existing.push(entry.hash);
|
|
1713
|
-
|
|
1378
|
+
hashesByFactId.set(entry.fact.id, existing);
|
|
1714
1379
|
}
|
|
1715
1380
|
}
|
|
1716
1381
|
|
|
1717
|
-
return
|
|
1382
|
+
return hashesByFactId;
|
|
1718
1383
|
}
|
|
1719
1384
|
|
|
1720
1385
|
/**
|
|
@@ -1793,33 +1458,12 @@ async function fetchExistingMemoriesForExtraction(
|
|
|
1793
1458
|
* Simple text-overlap scoring between a query and a candidate document.
|
|
1794
1459
|
* Returns the number of overlapping lowercase words.
|
|
1795
1460
|
*/
|
|
1796
|
-
|
|
1797
|
-
const queryWords = new Set(
|
|
1798
|
-
query.toLowerCase().split(/\s+/).filter((w) => w.length >= 2),
|
|
1799
|
-
);
|
|
1800
|
-
const docWords = docText.toLowerCase().split(/\s+/);
|
|
1801
|
-
let score = 0;
|
|
1802
|
-
for (const word of docWords) {
|
|
1803
|
-
if (queryWords.has(word)) score++;
|
|
1804
|
-
}
|
|
1805
|
-
return score;
|
|
1806
|
-
}
|
|
1461
|
+
// textScore extracted to ./runtime/format-helpers.ts.
|
|
1807
1462
|
|
|
1808
1463
|
/**
|
|
1809
1464
|
* Format a relative time string (e.g. "2 hours ago").
|
|
1810
1465
|
*/
|
|
1811
|
-
|
|
1812
|
-
const ms = typeof isoOrMs === 'number' ? isoOrMs : new Date(isoOrMs).getTime();
|
|
1813
|
-
const diffMs = Date.now() - ms;
|
|
1814
|
-
const seconds = Math.floor(diffMs / 1000);
|
|
1815
|
-
if (seconds < 60) return 'just now';
|
|
1816
|
-
const minutes = Math.floor(seconds / 60);
|
|
1817
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
1818
|
-
const hours = Math.floor(minutes / 60);
|
|
1819
|
-
if (hours < 24) return `${hours}h ago`;
|
|
1820
|
-
const days = Math.floor(hours / 24);
|
|
1821
|
-
return `${days}d ago`;
|
|
1822
|
-
}
|
|
1466
|
+
// relativeTime extracted to ./runtime/format-helpers.ts.
|
|
1823
1467
|
|
|
1824
1468
|
// ---------------------------------------------------------------------------
|
|
1825
1469
|
// Importance filter for auto-extraction
|
|
@@ -1833,47 +1477,7 @@ function relativeTime(isoOrMs: string | number): string {
|
|
|
1833
1477
|
* NOTE: This filter is ONLY applied to auto-extraction (hooks).
|
|
1834
1478
|
* The explicit `tr remember` CLI always stores regardless of importance.
|
|
1835
1479
|
*/
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
/**
|
|
1839
|
-
* Filter extracted facts by importance threshold.
|
|
1840
|
-
* Facts with importance < MIN_IMPORTANCE_THRESHOLD are dropped.
|
|
1841
|
-
* Facts with missing/undefined importance are treated as importance=5 (kept).
|
|
1842
|
-
*/
|
|
1843
|
-
function filterByImportance(
|
|
1844
|
-
facts: ExtractedFact[],
|
|
1845
|
-
logger: OpenClawPluginApi['logger'],
|
|
1846
|
-
): { kept: ExtractedFact[]; dropped: number } {
|
|
1847
|
-
const kept: ExtractedFact[] = [];
|
|
1848
|
-
let dropped = 0;
|
|
1849
|
-
|
|
1850
|
-
for (const fact of facts) {
|
|
1851
|
-
const importance = fact.importance ?? 5;
|
|
1852
|
-
if (importance >= MIN_IMPORTANCE_THRESHOLD) {
|
|
1853
|
-
kept.push(fact);
|
|
1854
|
-
} else {
|
|
1855
|
-
dropped++;
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
// Phase 2.2.5: always log the filter outcome so the agent_end path can
|
|
1860
|
-
// distinguish "LLM returned 0 facts" from "LLM returned N facts all dropped
|
|
1861
|
-
// below threshold" from "LLM returned N facts, all kept". Prior to 2.2.5
|
|
1862
|
-
// this only logged on drops, which made empty-input invisible.
|
|
1863
|
-
if (facts.length === 0) {
|
|
1864
|
-
logger.info('Importance filter: input=0 (nothing to filter)');
|
|
1865
|
-
} else if (dropped > 0) {
|
|
1866
|
-
logger.info(
|
|
1867
|
-
`Importance filter: dropped ${dropped}/${facts.length} facts below threshold ${MIN_IMPORTANCE_THRESHOLD}`,
|
|
1868
|
-
);
|
|
1869
|
-
} else {
|
|
1870
|
-
logger.info(
|
|
1871
|
-
`Importance filter: kept all ${facts.length} facts (threshold ${MIN_IMPORTANCE_THRESHOLD})`,
|
|
1872
|
-
);
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
return { kept, dropped };
|
|
1876
|
-
}
|
|
1480
|
+
// filterByImportance + MIN_IMPORTANCE_THRESHOLD extracted to ./extraction/importance-filter.ts.
|
|
1877
1481
|
|
|
1878
1482
|
// ---------------------------------------------------------------------------
|
|
1879
1483
|
// Auto-extraction helper
|
|
@@ -1904,10 +1508,10 @@ async function storeExtractedFacts(
|
|
|
1904
1508
|
|
|
1905
1509
|
for (const fact of facts) {
|
|
1906
1510
|
try {
|
|
1907
|
-
const
|
|
1908
|
-
if (
|
|
1909
|
-
embeddingMap.set(fact.text,
|
|
1910
|
-
embeddingResultMap.set(fact.text,
|
|
1511
|
+
const embeddingResult = await generateEmbeddingAndLSH(fact.text, logger);
|
|
1512
|
+
if (embeddingResult) {
|
|
1513
|
+
embeddingMap.set(fact.text, embeddingResult.embedding);
|
|
1514
|
+
embeddingResultMap.set(fact.text, embeddingResult);
|
|
1911
1515
|
}
|
|
1912
1516
|
} catch {
|
|
1913
1517
|
// Embedding generation failed for this fact -- proceed without it.
|
|
@@ -2261,12 +1865,12 @@ async function storeExtractedFacts(
|
|
|
2261
1865
|
for (let i = 0; i < pendingPayloads.length; i++) {
|
|
2262
1866
|
const slice = [pendingPayloads[i]]; // Single fact per UserOp
|
|
2263
1867
|
try {
|
|
2264
|
-
const
|
|
2265
|
-
if (
|
|
1868
|
+
const submitResult = await submitFactBatchOnChain(slice, batchConfig);
|
|
1869
|
+
if (submitResult.success) {
|
|
2266
1870
|
stored += slice.length;
|
|
2267
|
-
logger.info(`Fact ${i + 1}/${pendingPayloads.length}: submitted on-chain (tx=${
|
|
1871
|
+
logger.info(`Fact ${i + 1}/${pendingPayloads.length}: submitted on-chain (tx=${submitResult.txHash.slice(0, 10)}…)`);
|
|
2268
1872
|
} else {
|
|
2269
|
-
batchError = `On-chain batch submission failed (tx=${
|
|
1873
|
+
batchError = `On-chain batch submission failed (tx=${submitResult.txHash.slice(0, 10)}…)`;
|
|
2270
1874
|
logger.warn(batchError);
|
|
2271
1875
|
break; // Stop submitting remaining batches
|
|
2272
1876
|
}
|
|
@@ -2305,806 +1909,12 @@ async function storeExtractedFacts(
|
|
|
2305
1909
|
return stored;
|
|
2306
1910
|
}
|
|
2307
1911
|
|
|
2308
|
-
// ---------------------------------------------------------------------------
|
|
2309
|
-
// Import handler (for the registerCli `openclaw totalreclaw import-from` surface)
|
|
2310
|
-
// ---------------------------------------------------------------------------
|
|
2311
|
-
|
|
2312
|
-
/**
|
|
2313
|
-
* Handle import_from calls (CLI subcommand path; was the totalreclaw_import_from
|
|
2314
|
-
* agent tool before Phase 3.2 retired the agent tools).
|
|
2315
|
-
*
|
|
2316
|
-
* Two paths:
|
|
2317
|
-
* 1. Pre-structured sources (Mem0, MCP Memory) — adapter returns facts directly,
|
|
2318
|
-
* stored via storeExtractedFacts().
|
|
2319
|
-
* 2. Conversation-based sources (ChatGPT, Claude) — adapter returns conversation
|
|
2320
|
-
* chunks, each chunk is passed through extractFacts() (the same LLM extraction
|
|
2321
|
-
* pipeline used for auto-extraction), then stored via storeExtractedFacts().
|
|
2322
|
-
*/
|
|
2323
|
-
async function handlePluginImportFrom(
|
|
2324
|
-
params: Record<string, unknown>,
|
|
2325
|
-
logger: OpenClawPluginApi['logger'],
|
|
2326
|
-
): Promise<Record<string, unknown>> {
|
|
2327
|
-
_importInProgress = true;
|
|
2328
|
-
const startTime = Date.now();
|
|
2329
|
-
|
|
2330
|
-
const source = params.source as string;
|
|
2331
|
-
const validSources = ['mem0', 'mcp-memory', 'chatgpt', 'claude', 'gemini'];
|
|
2332
|
-
|
|
2333
|
-
if (!source || !validSources.includes(source)) {
|
|
2334
|
-
return { success: false, error: `Invalid source. Must be one of: ${validSources.join(', ')}` };
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
// Generate import_id up front so dry-run responses and background tasks share it.
|
|
2338
|
-
const importId = (params.resume_id as string | undefined) ?? crypto.randomUUID();
|
|
2339
|
-
|
|
2340
|
-
try {
|
|
2341
|
-
const { getAdapter } = await import('./import-adapters/index.js');
|
|
2342
|
-
const adapter = getAdapter(source as import('./import-adapters/types.js').ImportSource);
|
|
2343
|
-
|
|
2344
|
-
const parseResult = await adapter.parse({
|
|
2345
|
-
content: params.content as string | undefined,
|
|
2346
|
-
api_key: params.api_key as string | undefined,
|
|
2347
|
-
source_user_id: params.source_user_id as string | undefined,
|
|
2348
|
-
api_url: params.api_url as string | undefined,
|
|
2349
|
-
file_path: params.file_path as string | undefined,
|
|
2350
|
-
});
|
|
2351
|
-
|
|
2352
|
-
const hasChunks = parseResult.chunks && parseResult.chunks.length > 0;
|
|
2353
|
-
const hasFacts = parseResult.facts && parseResult.facts.length > 0;
|
|
2354
|
-
|
|
2355
|
-
if (parseResult.errors.length > 0 && !hasFacts && !hasChunks) {
|
|
2356
|
-
return {
|
|
2357
|
-
success: false,
|
|
2358
|
-
error: `Failed to parse ${adapter.displayName} data`,
|
|
2359
|
-
details: parseResult.errors,
|
|
2360
|
-
};
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
// Dry run: report what was parsed (chunks or facts)
|
|
2364
|
-
if (params.dry_run) {
|
|
2365
|
-
if (hasChunks) {
|
|
2366
|
-
const totalChunks = parseResult.chunks.length;
|
|
2367
|
-
const EXTRACTION_RATIO = 2.5; // avg facts per chunk, from empirical data
|
|
2368
|
-
const BATCH_SIZE = 25;
|
|
2369
|
-
const SECONDS_PER_BATCH = 45; // ~30s extraction + ~15s embed+store
|
|
2370
|
-
const estimatedFacts = Math.round(totalChunks * EXTRACTION_RATIO);
|
|
2371
|
-
const estimatedBatches = Math.ceil(totalChunks / BATCH_SIZE);
|
|
2372
|
-
const estimatedMinutes = Math.ceil(estimatedBatches * SECONDS_PER_BATCH / 60);
|
|
2373
|
-
|
|
2374
|
-
return {
|
|
2375
|
-
success: true,
|
|
2376
|
-
dry_run: true,
|
|
2377
|
-
import_id: importId,
|
|
2378
|
-
source,
|
|
2379
|
-
total_chunks: totalChunks,
|
|
2380
|
-
total_messages: parseResult.totalMessages,
|
|
2381
|
-
estimated_facts: estimatedFacts,
|
|
2382
|
-
estimated_batches: estimatedBatches,
|
|
2383
|
-
estimated_minutes: estimatedMinutes,
|
|
2384
|
-
batch_size: BATCH_SIZE,
|
|
2385
|
-
preview: parseResult.chunks.slice(0, 5).map((c) => ({
|
|
2386
|
-
title: c.title,
|
|
2387
|
-
messages: c.messages.length,
|
|
2388
|
-
first_message: c.messages[0]?.text.slice(0, 100),
|
|
2389
|
-
})),
|
|
2390
|
-
note: `Estimated ${estimatedFacts} facts from ${totalChunks} chunks (~${estimatedMinutes} min). Confirm to start background import.`,
|
|
2391
|
-
warnings: parseResult.warnings,
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
return {
|
|
2395
|
-
success: true,
|
|
2396
|
-
dry_run: true,
|
|
2397
|
-
import_id: importId,
|
|
2398
|
-
source,
|
|
2399
|
-
total_found: parseResult.facts.length,
|
|
2400
|
-
preview: parseResult.facts.slice(0, 10).map((f) => ({
|
|
2401
|
-
type: f.type,
|
|
2402
|
-
text: f.text.slice(0, 100),
|
|
2403
|
-
importance: f.importance,
|
|
2404
|
-
})),
|
|
2405
|
-
warnings: parseResult.warnings,
|
|
2406
|
-
};
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
// ── Path 1: Conversation chunks (ChatGPT, Claude, Gemini) — background execution ──
|
|
2410
|
-
if (hasChunks) {
|
|
2411
|
-
const totalChunks = parseResult.chunks.length;
|
|
2412
|
-
const BATCH_SIZE = 25;
|
|
2413
|
-
const SECONDS_PER_BATCH = 45;
|
|
2414
|
-
const estimatedBatches = Math.ceil(totalChunks / BATCH_SIZE);
|
|
2415
|
-
const estimatedMinutes = Math.ceil(estimatedBatches * SECONDS_PER_BATCH / 60);
|
|
2416
|
-
const estimatedTotalFacts = Math.round(totalChunks * 2.5);
|
|
2417
|
-
const now = new Date();
|
|
2418
|
-
|
|
2419
|
-
const initialState: ImportState = {
|
|
2420
|
-
import_id: importId,
|
|
2421
|
-
source,
|
|
2422
|
-
status: 'running',
|
|
2423
|
-
started_at: now.toISOString(),
|
|
2424
|
-
last_updated: now.toISOString(),
|
|
2425
|
-
total_chunks: totalChunks,
|
|
2426
|
-
total_messages: parseResult.totalMessages,
|
|
2427
|
-
batch_done: 0,
|
|
2428
|
-
batch_total: estimatedBatches,
|
|
2429
|
-
facts_stored: 0,
|
|
2430
|
-
facts_extracted: 0,
|
|
2431
|
-
dups_skipped: 0,
|
|
2432
|
-
errors: [],
|
|
2433
|
-
file_path: params.file_path as string | undefined,
|
|
2434
|
-
estimated_total_facts: estimatedTotalFacts,
|
|
2435
|
-
estimated_minutes: estimatedMinutes,
|
|
2436
|
-
estimated_completion_iso: new Date(now.getTime() + estimatedBatches * SECONDS_PER_BATCH * 1000).toISOString(),
|
|
2437
|
-
disclosure_confirmed: !!(params.disclosure_confirmed),
|
|
2438
|
-
};
|
|
2439
|
-
writeImportState(initialState);
|
|
2440
|
-
logger.info(`Import ${importId}: background task started (${totalChunks} chunks, ~${estimatedMinutes}min)`);
|
|
2441
|
-
|
|
2442
|
-
void handleChunkImport(
|
|
2443
|
-
parseResult.chunks, parseResult.totalMessages, source, logger, startTime, parseResult.warnings, importId,
|
|
2444
|
-
).catch((err: unknown) => {
|
|
2445
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2446
|
-
logger.warn(`Import ${importId}: background task failed: ${msg}`);
|
|
2447
|
-
const failedState = readImportState(importId);
|
|
2448
|
-
if (failedState && failedState.status === 'running') {
|
|
2449
|
-
writeImportState({ ...failedState, status: 'failed', errors: [...failedState.errors, msg] });
|
|
2450
|
-
}
|
|
2451
|
-
});
|
|
2452
|
-
|
|
2453
|
-
return {
|
|
2454
|
-
import_id: importId,
|
|
2455
|
-
status: 'running',
|
|
2456
|
-
source,
|
|
2457
|
-
total_chunks: totalChunks,
|
|
2458
|
-
estimated_batches: estimatedBatches,
|
|
2459
|
-
estimated_minutes: estimatedMinutes,
|
|
2460
|
-
estimated_completion_iso: initialState.estimated_completion_iso,
|
|
2461
|
-
message: `Import started in background. ~${estimatedMinutes} min for ${totalChunks} chunks. Check progress with \`openclaw totalreclaw import status\` on the gateway host (or \`import status --id ${importId} --json\` from an agent shell).`,
|
|
2462
|
-
warnings: parseResult.warnings,
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
// ── Path 2: Pre-structured facts (Mem0, MCP Memory) — direct store ──
|
|
2467
|
-
const extractedFacts: ExtractedFact[] = parseResult.facts.map((f) => ({
|
|
2468
|
-
text: f.text,
|
|
2469
|
-
type: f.type,
|
|
2470
|
-
importance: f.importance,
|
|
2471
|
-
action: 'ADD' as const,
|
|
2472
|
-
}));
|
|
2473
|
-
|
|
2474
|
-
// Store in batches of 50. Stop on any batch failure to prevent
|
|
2475
|
-
// nonce zombies from blocking subsequent UserOps (AA25).
|
|
2476
|
-
let totalStored = 0;
|
|
2477
|
-
let storeError: string | undefined;
|
|
2478
|
-
const batchSize = 50;
|
|
2479
|
-
|
|
2480
|
-
for (let i = 0; i < extractedFacts.length; i += batchSize) {
|
|
2481
|
-
const batch = extractedFacts.slice(i, i + batchSize);
|
|
2482
|
-
try {
|
|
2483
|
-
const stored = await storeExtractedFacts(batch, logger);
|
|
2484
|
-
totalStored += stored;
|
|
2485
|
-
|
|
2486
|
-
logger.info(
|
|
2487
|
-
`Import progress: ${Math.min(i + batchSize, extractedFacts.length)}/${extractedFacts.length} processed, ${totalStored} stored`,
|
|
2488
|
-
);
|
|
2489
|
-
} catch (err: unknown) {
|
|
2490
|
-
storeError = err instanceof Error ? err.message : String(err);
|
|
2491
|
-
logger.warn(`Import stopped at batch ${Math.floor(i / batchSize) + 1}: ${storeError}`);
|
|
2492
|
-
break; // Stop processing further batches
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
const importWarnings = [...parseResult.warnings];
|
|
2497
|
-
if (storeError) {
|
|
2498
|
-
importWarnings.push(`Import stopped early: ${storeError}`);
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
return {
|
|
2502
|
-
success: totalStored > 0,
|
|
2503
|
-
source,
|
|
2504
|
-
import_id: importId,
|
|
2505
|
-
total_found: parseResult.facts.length,
|
|
2506
|
-
imported: totalStored,
|
|
2507
|
-
skipped: parseResult.facts.length - totalStored,
|
|
2508
|
-
stopped_early: !!storeError,
|
|
2509
|
-
warnings: importWarnings,
|
|
2510
|
-
duration_ms: Date.now() - startTime,
|
|
2511
|
-
};
|
|
2512
|
-
} catch (e) {
|
|
2513
|
-
const msg = e instanceof Error ? e.message : 'Unknown error';
|
|
2514
|
-
logger.error(`Import failed: ${msg}`);
|
|
2515
|
-
return { success: false, error: `Import failed: ${msg}` };
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
1912
|
|
|
2519
1913
|
// ---------------------------------------------------------------------------
|
|
2520
|
-
//
|
|
1914
|
+
// Import subsystem extracted to ./import/import-runtime.ts.
|
|
1915
|
+
// storeExtractedFacts is injected below (it closes over plugin session state).
|
|
2521
1916
|
// ---------------------------------------------------------------------------
|
|
2522
|
-
|
|
2523
|
-
// Lazy-load WASM for smart import functions (same pattern as crypto.ts /
|
|
2524
|
-
// subgraph-store.ts). Goes through __cjsRequire (createRequire(import.meta.url))
|
|
2525
|
-
// declared at the top of the file — bare `require()` is undefined under
|
|
2526
|
-
// pure-ESM Node, see issue #124.
|
|
2527
|
-
let _smartImportWasm: typeof import('@totalreclaw/core') | null = null;
|
|
2528
|
-
function getSmartImportWasm() {
|
|
2529
|
-
if (!_smartImportWasm) _smartImportWasm = __cjsRequire('@totalreclaw/core');
|
|
2530
|
-
return _smartImportWasm;
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
/**
|
|
2534
|
-
* Check whether the @totalreclaw/core WASM module exposes smart import functions.
|
|
2535
|
-
* Returns false if the module is an older version without smart import support.
|
|
2536
|
-
*/
|
|
2537
|
-
function hasSmartImportSupport(): boolean {
|
|
2538
|
-
try {
|
|
2539
|
-
const wasm = getSmartImportWasm();
|
|
2540
|
-
return typeof wasm.chunksToSummaries === 'function' &&
|
|
2541
|
-
typeof wasm.buildProfileBatchPrompt === 'function' &&
|
|
2542
|
-
typeof wasm.parseProfileBatchResponse === 'function' &&
|
|
2543
|
-
typeof wasm.buildTriagePrompt === 'function' &&
|
|
2544
|
-
typeof wasm.parseTriageResponse === 'function' &&
|
|
2545
|
-
typeof wasm.enrichExtractionPrompt === 'function';
|
|
2546
|
-
} catch {
|
|
2547
|
-
return false;
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
/** Smart import result containing profile, triage decisions, and enriched system prompt. */
|
|
2552
|
-
interface SmartImportContext {
|
|
2553
|
-
/** JSON-serialized UserProfile (for WASM calls that require profile_json) */
|
|
2554
|
-
profileJson: string;
|
|
2555
|
-
/** Triage decisions indexed by chunk_index */
|
|
2556
|
-
decisions: Array<{ chunk_index: number; decision: string; reason: string }>;
|
|
2557
|
-
/** Enriched system prompt for extraction (profile context injected) */
|
|
2558
|
-
enrichedSystemPrompt: string;
|
|
2559
|
-
/** Number of chunks marked for extraction */
|
|
2560
|
-
extractCount: number;
|
|
2561
|
-
/** Number of chunks marked for skipping */
|
|
2562
|
-
skipCount: number;
|
|
2563
|
-
/** Duration of the profiling + triage pipeline in ms */
|
|
2564
|
-
durationMs: number;
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
/**
|
|
2568
|
-
* Run the smart import two-pass pipeline: profile the user from conversation
|
|
2569
|
-
* summaries, then triage chunks as EXTRACT or SKIP.
|
|
2570
|
-
*
|
|
2571
|
-
* All prompt construction and response parsing happens in @totalreclaw/core WASM.
|
|
2572
|
-
* LLM calls use the plugin's existing chatCompletion() function.
|
|
2573
|
-
*
|
|
2574
|
-
* Returns null if smart import is unavailable (old WASM, no LLM config, etc.)
|
|
2575
|
-
* so the caller can fall back to blind extraction.
|
|
2576
|
-
*/
|
|
2577
|
-
async function runSmartImportPipeline(
|
|
2578
|
-
chunks: import('./import-adapters/types.js').ConversationChunk[],
|
|
2579
|
-
logger: { info: (msg: string) => void; warn: (msg: string) => void },
|
|
2580
|
-
): Promise<SmartImportContext | null> {
|
|
2581
|
-
// Guard: WASM must have smart import functions
|
|
2582
|
-
if (!hasSmartImportSupport()) {
|
|
2583
|
-
logger.info('Smart import: WASM module does not support smart import, falling back to blind extraction');
|
|
2584
|
-
return null;
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
// Guard: LLM must be available
|
|
2588
|
-
const llmConfig = resolveLLMConfig();
|
|
2589
|
-
if (!llmConfig) {
|
|
2590
|
-
logger.info('Smart import: no LLM available, falling back to blind extraction');
|
|
2591
|
-
return null;
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
const pipelineStart = Date.now();
|
|
2595
|
-
const wasm = getSmartImportWasm();
|
|
2596
|
-
|
|
2597
|
-
try {
|
|
2598
|
-
// Step 0: Convert chunks to compact summaries (first + last message)
|
|
2599
|
-
const wasmChunks = chunks.map((c, i) => ({
|
|
2600
|
-
index: i,
|
|
2601
|
-
title: c.title || 'Untitled',
|
|
2602
|
-
messages: c.messages.map((m) => ({ role: m.role, content: m.text })),
|
|
2603
|
-
timestamp: c.timestamp || null,
|
|
2604
|
-
}));
|
|
2605
|
-
const summaries = wasm.chunksToSummaries(JSON.stringify(wasmChunks));
|
|
2606
|
-
const summariesJson = JSON.stringify(summaries);
|
|
2607
|
-
|
|
2608
|
-
// Step 1: Build user profile (batch summarize -> merge)
|
|
2609
|
-
const PROFILE_BATCH_SIZE = 50;
|
|
2610
|
-
const profileStart = Date.now();
|
|
2611
|
-
const partials: unknown[] = [];
|
|
2612
|
-
|
|
2613
|
-
for (let i = 0; i < summaries.length; i += PROFILE_BATCH_SIZE) {
|
|
2614
|
-
const batch = summaries.slice(i, i + PROFILE_BATCH_SIZE);
|
|
2615
|
-
const prompt = wasm.buildProfileBatchPrompt(JSON.stringify(batch));
|
|
2616
|
-
const response = await chatCompletion(llmConfig, [
|
|
2617
|
-
{ role: 'user', content: prompt },
|
|
2618
|
-
], { maxTokens: 2048, temperature: 0 });
|
|
2619
|
-
|
|
2620
|
-
if (!response) {
|
|
2621
|
-
logger.warn(`Smart import: LLM returned empty response for profile batch ${Math.floor(i / PROFILE_BATCH_SIZE) + 1}`);
|
|
2622
|
-
continue;
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
const partial = wasm.parseProfileBatchResponse(response);
|
|
2626
|
-
partials.push(partial);
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
if (partials.length === 0) {
|
|
2630
|
-
logger.warn('Smart import: no profile batches produced, falling back to blind extraction');
|
|
2631
|
-
return null;
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
let profile: unknown;
|
|
2635
|
-
if (partials.length === 1) {
|
|
2636
|
-
// Single batch — skip merge, promote partial to full profile
|
|
2637
|
-
// parseProfileBatchResponse returns a PartialProfile; convert to UserProfile shape
|
|
2638
|
-
const p = partials[0] as Record<string, unknown>;
|
|
2639
|
-
profile = {
|
|
2640
|
-
identity: p.identity ?? null,
|
|
2641
|
-
themes: p.themes ?? [],
|
|
2642
|
-
projects: p.projects ?? [],
|
|
2643
|
-
stack: p.stack ?? [],
|
|
2644
|
-
decisions: p.decisions ?? [],
|
|
2645
|
-
interests: p.interests ?? [],
|
|
2646
|
-
skip_patterns: p.skip_patterns ?? [],
|
|
2647
|
-
};
|
|
2648
|
-
} else {
|
|
2649
|
-
const mergePrompt = wasm.buildProfileMergePrompt(JSON.stringify(partials));
|
|
2650
|
-
const mergeResponse = await chatCompletion(llmConfig, [
|
|
2651
|
-
{ role: 'user', content: mergePrompt },
|
|
2652
|
-
], { maxTokens: 2048, temperature: 0 });
|
|
2653
|
-
|
|
2654
|
-
if (!mergeResponse) {
|
|
2655
|
-
logger.warn('Smart import: LLM returned empty response for profile merge, falling back to blind extraction');
|
|
2656
|
-
return null;
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
profile = wasm.parseProfileResponse(mergeResponse);
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2662
|
-
const profileJson = JSON.stringify(profile);
|
|
2663
|
-
const profileDuration = Date.now() - profileStart;
|
|
2664
|
-
|
|
2665
|
-
const p = profile as Record<string, unknown>;
|
|
2666
|
-
const themeCount = Array.isArray(p.themes) ? p.themes.length : 0;
|
|
2667
|
-
const skipPatternCount = Array.isArray(p.skip_patterns) ? p.skip_patterns.length : 0;
|
|
2668
|
-
logger.info(
|
|
2669
|
-
`Smart import: profile built in ${profileDuration}ms (themes=${themeCount}, skip_patterns=${skipPatternCount})`,
|
|
2670
|
-
);
|
|
2671
|
-
|
|
2672
|
-
// Step 1.5: Chunk triage (EXTRACT or SKIP)
|
|
2673
|
-
const triageStart = Date.now();
|
|
2674
|
-
const allDecisions: Array<{ chunk_index: number; decision: string; reason: string }> = [];
|
|
2675
|
-
const TRIAGE_BATCH_SIZE = 50;
|
|
2676
|
-
|
|
2677
|
-
for (let i = 0; i < summaries.length; i += TRIAGE_BATCH_SIZE) {
|
|
2678
|
-
const batch = summaries.slice(i, i + TRIAGE_BATCH_SIZE);
|
|
2679
|
-
const triagePrompt = wasm.buildTriagePrompt(profileJson, JSON.stringify(batch));
|
|
2680
|
-
const triageResponse = await chatCompletion(llmConfig, [
|
|
2681
|
-
{ role: 'user', content: triagePrompt },
|
|
2682
|
-
], { maxTokens: 4096, temperature: 0 });
|
|
2683
|
-
|
|
2684
|
-
if (!triageResponse) {
|
|
2685
|
-
logger.warn(`Smart import: LLM returned empty response for triage batch ${Math.floor(i / TRIAGE_BATCH_SIZE) + 1}, defaulting to EXTRACT`);
|
|
2686
|
-
// Default all chunks in this batch to EXTRACT
|
|
2687
|
-
for (let j = i; j < Math.min(i + TRIAGE_BATCH_SIZE, summaries.length); j++) {
|
|
2688
|
-
allDecisions.push({ chunk_index: j, decision: 'EXTRACT', reason: 'triage LLM unavailable' });
|
|
2689
|
-
}
|
|
2690
|
-
continue;
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
const batchDecisions = wasm.parseTriageResponse(triageResponse) as Array<{
|
|
2694
|
-
chunk_index: number;
|
|
2695
|
-
decision: string;
|
|
2696
|
-
reason: string;
|
|
2697
|
-
}>;
|
|
2698
|
-
allDecisions.push(...batchDecisions);
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
const triageDuration = Date.now() - triageStart;
|
|
2702
|
-
|
|
2703
|
-
const extractCount = allDecisions.filter((d) => d.decision !== 'SKIP').length;
|
|
2704
|
-
const skipCount = allDecisions.filter((d) => d.decision === 'SKIP').length;
|
|
2705
|
-
logger.info(
|
|
2706
|
-
`Smart import: triage complete in ${triageDuration}ms (extract=${extractCount}, skip=${skipCount}, total=${chunks.length})`,
|
|
2707
|
-
);
|
|
2708
|
-
|
|
2709
|
-
// Step 2: Build enriched system prompt for extraction
|
|
2710
|
-
const enrichedSystemPrompt = wasm.enrichExtractionPrompt(profileJson, EXTRACTION_SYSTEM_PROMPT);
|
|
2711
|
-
|
|
2712
|
-
const totalDuration = Date.now() - pipelineStart;
|
|
2713
|
-
logger.info(`Smart import: pipeline complete in ${totalDuration}ms`);
|
|
2714
|
-
|
|
2715
|
-
return {
|
|
2716
|
-
profileJson,
|
|
2717
|
-
decisions: allDecisions,
|
|
2718
|
-
enrichedSystemPrompt,
|
|
2719
|
-
extractCount,
|
|
2720
|
-
skipCount,
|
|
2721
|
-
durationMs: totalDuration,
|
|
2722
|
-
};
|
|
2723
|
-
} catch (err) {
|
|
2724
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2725
|
-
logger.warn(`Smart import: pipeline failed (${msg}), falling back to blind extraction`);
|
|
2726
|
-
return null;
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
/**
|
|
2731
|
-
* Check if a chunk should be skipped based on triage decisions.
|
|
2732
|
-
* If no decision exists for the chunk index, defaults to EXTRACT (safe default).
|
|
2733
|
-
*/
|
|
2734
|
-
function isChunkSkipped(
|
|
2735
|
-
chunkIndex: number,
|
|
2736
|
-
decisions: Array<{ chunk_index: number; decision: string }>,
|
|
2737
|
-
): { skipped: boolean; reason: string } {
|
|
2738
|
-
const decision = decisions.find((d) => d.chunk_index === chunkIndex);
|
|
2739
|
-
if (decision && decision.decision === 'SKIP') {
|
|
2740
|
-
return { skipped: true, reason: (decision as { reason?: string }).reason || 'triage: skip' };
|
|
2741
|
-
}
|
|
2742
|
-
return { skipped: false, reason: '' };
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
/**
|
|
2746
|
-
* Process a batch (slice) of conversation chunks from a file.
|
|
2747
|
-
* Called repeatedly by the agent for large imports.
|
|
2748
|
-
*/
|
|
2749
|
-
async function handleBatchImport(
|
|
2750
|
-
params: Record<string, unknown>,
|
|
2751
|
-
logger: OpenClawPluginApi['logger'],
|
|
2752
|
-
): Promise<Record<string, unknown>> {
|
|
2753
|
-
_importInProgress = true;
|
|
2754
|
-
const source = params.source as string;
|
|
2755
|
-
const filePath = params.file_path as string | undefined;
|
|
2756
|
-
const content = params.content as string | undefined;
|
|
2757
|
-
const offset = (params.offset as number) ?? 0;
|
|
2758
|
-
const batchSize = (params.batch_size as number) ?? 25;
|
|
2759
|
-
|
|
2760
|
-
const validSources = ['mem0', 'mcp-memory', 'chatgpt', 'claude', 'gemini'];
|
|
2761
|
-
if (!source || !validSources.includes(source)) {
|
|
2762
|
-
return { success: false, error: `Invalid source. Must be one of: ${validSources.join(', ')}` };
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
const startTime = Date.now();
|
|
2766
|
-
|
|
2767
|
-
const { getAdapter } = await import('./import-adapters/index.js');
|
|
2768
|
-
const adapter = getAdapter(source as import('./import-adapters/types.js').ImportSource);
|
|
2769
|
-
|
|
2770
|
-
const parseResult = await adapter.parse({ content, file_path: filePath });
|
|
2771
|
-
|
|
2772
|
-
if (parseResult.errors.length > 0 && parseResult.chunks.length === 0) {
|
|
2773
|
-
return { success: false, error: parseResult.errors.join('; ') };
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
const totalChunks = parseResult.chunks.length;
|
|
2777
|
-
const slice = parseResult.chunks.slice(offset, offset + batchSize);
|
|
2778
|
-
const remaining = Math.max(0, totalChunks - offset - slice.length);
|
|
2779
|
-
|
|
2780
|
-
// --- Smart Import: Profile + Triage ---
|
|
2781
|
-
// Build profile from ALL chunks (not just the slice) for full context,
|
|
2782
|
-
// then triage only the current slice. For simplicity, we rebuild on every
|
|
2783
|
-
// batch call — optimization (caching) can come later.
|
|
2784
|
-
const smartCtx = await runSmartImportPipeline(parseResult.chunks, logger);
|
|
2785
|
-
let chunksSkipped = 0;
|
|
2786
|
-
|
|
2787
|
-
// Process the slice through the normal extraction + storage pipeline.
|
|
2788
|
-
// If a batch fails (nonce zombie, quota exceeded, etc.), stop immediately
|
|
2789
|
-
// to prevent subsequent UserOps from hitting AA25 nonce conflicts.
|
|
2790
|
-
let factsExtracted = 0;
|
|
2791
|
-
let factsStored = 0;
|
|
2792
|
-
let chunksProcessed = 0;
|
|
2793
|
-
let storeError: string | undefined;
|
|
2794
|
-
|
|
2795
|
-
for (let i = 0; i < slice.length; i++) {
|
|
2796
|
-
const chunk = slice[i];
|
|
2797
|
-
const globalIndex = offset + i; // Index in the full chunks array
|
|
2798
|
-
|
|
2799
|
-
// Smart import: skip chunks triaged as SKIP
|
|
2800
|
-
if (smartCtx) {
|
|
2801
|
-
const { skipped, reason } = isChunkSkipped(globalIndex, smartCtx.decisions);
|
|
2802
|
-
if (skipped) {
|
|
2803
|
-
logger.info(`Import: skipping chunk ${globalIndex + 1}/${totalChunks}: "${chunk.title}" (${reason})`);
|
|
2804
|
-
chunksSkipped++;
|
|
2805
|
-
chunksProcessed++;
|
|
2806
|
-
continue;
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
|
|
2810
|
-
logger.info(`Import: extracting facts from chunk ${globalIndex + 1}/${totalChunks}: "${chunk.title}"`);
|
|
2811
|
-
|
|
2812
|
-
const messages = chunk.messages.map((m) => ({ role: m.role, content: m.text }));
|
|
2813
|
-
const facts = await extractFacts(
|
|
2814
|
-
messages,
|
|
2815
|
-
'full',
|
|
2816
|
-
undefined, // no existing memories for dedup during import
|
|
2817
|
-
smartCtx?.enrichedSystemPrompt, // profile-enriched extraction prompt
|
|
2818
|
-
);
|
|
2819
|
-
chunksProcessed++;
|
|
2820
|
-
|
|
2821
|
-
if (facts.length > 0) {
|
|
2822
|
-
factsExtracted += facts.length;
|
|
2823
|
-
try {
|
|
2824
|
-
const stored = await storeExtractedFacts(facts, logger);
|
|
2825
|
-
factsStored += stored;
|
|
2826
|
-
} catch (err: unknown) {
|
|
2827
|
-
storeError = err instanceof Error ? err.message : String(err);
|
|
2828
|
-
logger.warn(`Import batch stopped at chunk ${globalIndex + 1}/${totalChunks}: ${storeError}`);
|
|
2829
|
-
break; // Stop processing further chunks — a zombie UserOp may block writes
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
return {
|
|
2835
|
-
success: factsStored > 0 || (!storeError && factsExtracted === 0),
|
|
2836
|
-
batch_offset: offset,
|
|
2837
|
-
batch_size: chunksProcessed,
|
|
2838
|
-
total_chunks: totalChunks,
|
|
2839
|
-
facts_extracted: factsExtracted,
|
|
2840
|
-
facts_stored: factsStored,
|
|
2841
|
-
chunks_skipped: chunksSkipped,
|
|
2842
|
-
remaining_chunks: remaining,
|
|
2843
|
-
is_complete: remaining === 0 && !storeError,
|
|
2844
|
-
stopped_early: !!storeError,
|
|
2845
|
-
error: storeError,
|
|
2846
|
-
smart_import: smartCtx ? {
|
|
2847
|
-
profile_duration_ms: smartCtx.durationMs,
|
|
2848
|
-
extract_count: smartCtx.extractCount,
|
|
2849
|
-
skip_count: smartCtx.skipCount,
|
|
2850
|
-
} : null,
|
|
2851
|
-
// Estimation for the full import
|
|
2852
|
-
estimated_total_facts: Math.round(totalChunks * 2.5),
|
|
2853
|
-
estimated_total_userops: Math.ceil(totalChunks * 2.5 / 15),
|
|
2854
|
-
estimated_minutes: Math.ceil(Math.ceil(totalChunks / batchSize) * 45 / 60),
|
|
2855
|
-
duration_ms: Date.now() - startTime,
|
|
2856
|
-
};
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
/**
|
|
2860
|
-
* Process conversation chunks through LLM extraction and store results.
|
|
2861
|
-
*
|
|
2862
|
-
* Each chunk is passed to extractFacts() — the same extraction pipeline used
|
|
2863
|
-
* for auto-extraction during live conversations. This ensures import quality
|
|
2864
|
-
* matches conversation extraction quality.
|
|
2865
|
-
*/
|
|
2866
|
-
async function handleChunkImport(
|
|
2867
|
-
chunks: import('./import-adapters/types.js').ConversationChunk[],
|
|
2868
|
-
totalMessages: number,
|
|
2869
|
-
source: string,
|
|
2870
|
-
logger: OpenClawPluginApi['logger'],
|
|
2871
|
-
startTime: number,
|
|
2872
|
-
warnings: string[],
|
|
2873
|
-
importId?: string,
|
|
2874
|
-
): Promise<Record<string, unknown>> {
|
|
2875
|
-
let totalExtracted = 0;
|
|
2876
|
-
let totalStored = 0;
|
|
2877
|
-
let chunksProcessed = 0;
|
|
2878
|
-
let chunksSkipped = 0;
|
|
2879
|
-
const resolvedImportId = importId ?? crypto.randomUUID();
|
|
2880
|
-
|
|
2881
|
-
let storeError: string | undefined;
|
|
2882
|
-
|
|
2883
|
-
// --- Smart Import: Profile + Triage ---
|
|
2884
|
-
const smartCtx = await runSmartImportPipeline(chunks, logger);
|
|
2885
|
-
|
|
2886
|
-
const CHECKPOINT_EVERY = 25; // write state file every N chunks
|
|
2887
|
-
|
|
2888
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
2889
|
-
// Check abort flag from state file before each chunk (background task may be cancelled).
|
|
2890
|
-
if (importId) {
|
|
2891
|
-
const currentState = readImportState(importId);
|
|
2892
|
-
if (currentState?.status === 'aborted') {
|
|
2893
|
-
logger.info(`Import ${importId}: abort flag detected at chunk ${i + 1}/${chunks.length}, stopping`);
|
|
2894
|
-
break;
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
const chunk = chunks[i];
|
|
2899
|
-
chunksProcessed++;
|
|
2900
|
-
|
|
2901
|
-
// Smart import: skip chunks triaged as SKIP
|
|
2902
|
-
if (smartCtx) {
|
|
2903
|
-
const { skipped, reason } = isChunkSkipped(i, smartCtx.decisions);
|
|
2904
|
-
if (skipped) {
|
|
2905
|
-
logger.info(
|
|
2906
|
-
`Import: skipping chunk ${chunksProcessed}/${chunks.length}: "${chunk.title}" (${reason})`,
|
|
2907
|
-
);
|
|
2908
|
-
chunksSkipped++;
|
|
2909
|
-
continue;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
logger.info(
|
|
2914
|
-
`Import: extracting facts from chunk ${chunksProcessed}/${chunks.length}: "${chunk.title}"`,
|
|
2915
|
-
);
|
|
2916
|
-
|
|
2917
|
-
// Convert chunk messages to the format extractFacts() expects.
|
|
2918
|
-
// extractFacts() takes an array of message-like objects with { role, content }.
|
|
2919
|
-
const messages = chunk.messages.map((m) => ({
|
|
2920
|
-
role: m.role,
|
|
2921
|
-
content: m.text,
|
|
2922
|
-
}));
|
|
2923
|
-
|
|
2924
|
-
// Use 'full' mode to extract ALL valuable memories from the chunk
|
|
2925
|
-
// (not just the last few messages like 'turn' mode does).
|
|
2926
|
-
// Smart import: pass enriched system prompt with user profile context.
|
|
2927
|
-
const facts = await extractFacts(
|
|
2928
|
-
messages,
|
|
2929
|
-
'full',
|
|
2930
|
-
undefined, // no existing memories for dedup during import
|
|
2931
|
-
smartCtx?.enrichedSystemPrompt, // profile-enriched extraction prompt
|
|
2932
|
-
);
|
|
2933
|
-
|
|
2934
|
-
if (facts.length > 0) {
|
|
2935
|
-
totalExtracted += facts.length;
|
|
2936
|
-
|
|
2937
|
-
try {
|
|
2938
|
-
// Store through the normal pipeline (dedup, encrypt, store).
|
|
2939
|
-
// storeExtractedFacts throws on batch failure to prevent nonce zombies.
|
|
2940
|
-
const stored = await storeExtractedFacts(facts, logger);
|
|
2941
|
-
totalStored += stored;
|
|
2942
|
-
|
|
2943
|
-
logger.info(
|
|
2944
|
-
`Import chunk ${chunksProcessed}/${chunks.length}: extracted ${facts.length} facts, stored ${stored}`,
|
|
2945
|
-
);
|
|
2946
|
-
} catch (err: unknown) {
|
|
2947
|
-
storeError = err instanceof Error ? err.message : String(err);
|
|
2948
|
-
logger.warn(`Import stopped at chunk ${chunksProcessed}/${chunks.length}: ${storeError}`);
|
|
2949
|
-
break; // Stop processing further chunks — a zombie UserOp may block writes
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
// Checkpoint state file periodically so _import_status reflects live progress.
|
|
2954
|
-
if (importId && chunksProcessed % CHECKPOINT_EVERY === 0) {
|
|
2955
|
-
const liveState = readImportState(importId);
|
|
2956
|
-
if (liveState) {
|
|
2957
|
-
const estimatedBatches = liveState.batch_total || 1;
|
|
2958
|
-
const doneBatches = Math.floor(chunksProcessed / CHECKPOINT_EVERY);
|
|
2959
|
-
const elapsed = Date.now() - new Date(liveState.started_at).getTime();
|
|
2960
|
-
const secPerBatch = doneBatches > 0 ? elapsed / 1000 / doneBatches : 45;
|
|
2961
|
-
const remaining = estimatedBatches - doneBatches;
|
|
2962
|
-
const etaMs = remaining * secPerBatch * 1000;
|
|
2963
|
-
writeImportState({
|
|
2964
|
-
...liveState,
|
|
2965
|
-
batch_done: doneBatches,
|
|
2966
|
-
facts_stored: totalStored,
|
|
2967
|
-
facts_extracted: totalExtracted,
|
|
2968
|
-
estimated_completion_iso: new Date(Date.now() + etaMs).toISOString(),
|
|
2969
|
-
});
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
|
-
if (totalExtracted === 0 && chunks.length > 0 && !storeError && chunksSkipped < chunks.length) {
|
|
2975
|
-
warnings.push(
|
|
2976
|
-
`Processed ${chunks.length} conversation chunks (${totalMessages} messages) but the LLM ` +
|
|
2977
|
-
`did not extract any facts worth storing. This can happen if the conversations are mostly ` +
|
|
2978
|
-
`generic/ephemeral content without personal facts, preferences, or decisions.`,
|
|
2979
|
-
);
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
|
-
if (storeError) {
|
|
2983
|
-
warnings.push(`Import stopped early: ${storeError}. ${chunks.length - chunksProcessed} chunk(s) not processed.`);
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
// Final state file write for background imports.
|
|
2987
|
-
if (importId) {
|
|
2988
|
-
const finalState = readImportState(importId);
|
|
2989
|
-
if (finalState) {
|
|
2990
|
-
const finalStatus = storeError ? 'failed' : (finalState.status === 'aborted' ? 'aborted' : 'completed');
|
|
2991
|
-
writeImportState({
|
|
2992
|
-
...finalState,
|
|
2993
|
-
status: finalStatus,
|
|
2994
|
-
batch_done: finalState.batch_total,
|
|
2995
|
-
facts_stored: totalStored,
|
|
2996
|
-
facts_extracted: totalExtracted,
|
|
2997
|
-
errors: storeError ? [...finalState.errors, storeError] : finalState.errors,
|
|
2998
|
-
});
|
|
2999
|
-
}
|
|
3000
|
-
_importInProgress = false;
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
return {
|
|
3004
|
-
success: totalStored > 0 || totalExtracted > 0,
|
|
3005
|
-
source,
|
|
3006
|
-
import_id: resolvedImportId,
|
|
3007
|
-
total_chunks: chunks.length,
|
|
3008
|
-
chunks_processed: chunksProcessed,
|
|
3009
|
-
chunks_skipped: chunksSkipped,
|
|
3010
|
-
total_messages: totalMessages,
|
|
3011
|
-
facts_extracted: totalExtracted,
|
|
3012
|
-
imported: totalStored,
|
|
3013
|
-
skipped: totalExtracted - totalStored,
|
|
3014
|
-
stopped_early: !!storeError,
|
|
3015
|
-
smart_import: smartCtx ? {
|
|
3016
|
-
profile_duration_ms: smartCtx.durationMs,
|
|
3017
|
-
extract_count: smartCtx.extractCount,
|
|
3018
|
-
skip_count: smartCtx.skipCount,
|
|
3019
|
-
} : null,
|
|
3020
|
-
warnings,
|
|
3021
|
-
duration_ms: Date.now() - startTime,
|
|
3022
|
-
};
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
// ---------------------------------------------------------------------------
|
|
3026
|
-
// Import status + abort handlers
|
|
3027
|
-
// ---------------------------------------------------------------------------
|
|
3028
|
-
|
|
3029
|
-
async function handleImportStatus(
|
|
3030
|
-
params: Record<string, unknown>,
|
|
3031
|
-
logger: OpenClawPluginApi['logger'],
|
|
3032
|
-
): Promise<Record<string, unknown>> {
|
|
3033
|
-
const importId = params.import_id as string | undefined;
|
|
3034
|
-
|
|
3035
|
-
let state: ImportState | null;
|
|
3036
|
-
if (importId) {
|
|
3037
|
-
state = readImportState(importId);
|
|
3038
|
-
if (!state) return { error: `No import found with id: ${importId}` };
|
|
3039
|
-
} else {
|
|
3040
|
-
state = readMostRecentActiveImport();
|
|
3041
|
-
if (!state) return { status: 'no_active_import', message: 'No active import found. Start one with `openclaw totalreclaw import from <source>` on the gateway host. (Auto-resume still picks up running imports on gateway restart.)' };
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
// 1h freshness guard: mark stale imports as failed and prompt user to resume.
|
|
3045
|
-
if (state.status === 'running' && isImportStale(state)) {
|
|
3046
|
-
writeImportState({ ...state, status: 'failed', errors: [...state.errors, 'Stale: no progress in 1h'] });
|
|
3047
|
-
logger.info(`Import ${state.import_id}: marked stale (no progress in 1h)`);
|
|
3048
|
-
return {
|
|
3049
|
-
import_id: state.import_id,
|
|
3050
|
-
status: 'failed',
|
|
3051
|
-
stale: true,
|
|
3052
|
-
facts_stored: state.facts_stored,
|
|
3053
|
-
message: 'Import appears stale — no progress in 1 hour. Resume it with `openclaw totalreclaw import from <source> --file <path> --resume ' + state.import_id + '` on the gateway host, or restart the gateway to trigger auto-resume.',
|
|
3054
|
-
resume_id: state.import_id,
|
|
3055
|
-
};
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
const now = Date.now();
|
|
3059
|
-
const elapsedMs = now - new Date(state.started_at).getTime();
|
|
3060
|
-
const secPerBatch = state.batch_done > 0 ? elapsedMs / 1000 / state.batch_done : 45;
|
|
3061
|
-
const remaining = Math.max(0, state.batch_total - state.batch_done);
|
|
3062
|
-
const etaSeconds = state.status === 'running' ? Math.round(remaining * secPerBatch) : 0;
|
|
3063
|
-
|
|
3064
|
-
return {
|
|
3065
|
-
import_id: state.import_id,
|
|
3066
|
-
status: state.status,
|
|
3067
|
-
batch_done: state.batch_done,
|
|
3068
|
-
batch_total: state.batch_total,
|
|
3069
|
-
facts_stored: state.facts_stored,
|
|
3070
|
-
dups_skipped: state.dups_skipped,
|
|
3071
|
-
eta_seconds: etaSeconds,
|
|
3072
|
-
completion_iso: state.status === 'running'
|
|
3073
|
-
? new Date(now + etaSeconds * 1000).toISOString()
|
|
3074
|
-
: state.last_updated,
|
|
3075
|
-
source: state.source,
|
|
3076
|
-
started_at: state.started_at,
|
|
3077
|
-
errors: state.errors,
|
|
3078
|
-
};
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
async function handleImportAbort(
|
|
3082
|
-
params: Record<string, unknown>,
|
|
3083
|
-
logger: OpenClawPluginApi['logger'],
|
|
3084
|
-
): Promise<Record<string, unknown>> {
|
|
3085
|
-
const importId = params.import_id as string | undefined;
|
|
3086
|
-
if (!importId) return { error: 'import_id is required' };
|
|
3087
|
-
|
|
3088
|
-
const state = readImportState(importId);
|
|
3089
|
-
if (!state) return { error: `No import found with id: ${importId}` };
|
|
3090
|
-
|
|
3091
|
-
if (state.status === 'aborted') {
|
|
3092
|
-
return { aborted: true, idempotent: true, import_id: importId, facts_already_stored: state.facts_stored };
|
|
3093
|
-
}
|
|
3094
|
-
if (state.status === 'completed') {
|
|
3095
|
-
return { error: 'Import already completed — nothing to abort', import_id: importId, facts_stored: state.facts_stored };
|
|
3096
|
-
}
|
|
3097
|
-
|
|
3098
|
-
writeImportState({ ...state, status: 'aborted' });
|
|
3099
|
-
logger.info(`Import ${importId}: abort requested (${state.facts_stored} facts already stored)`);
|
|
3100
|
-
|
|
3101
|
-
return {
|
|
3102
|
-
aborted: true,
|
|
3103
|
-
import_id: importId,
|
|
3104
|
-
facts_already_stored: state.facts_stored,
|
|
3105
|
-
message: 'Import abort requested. The background task will stop at the next chunk boundary. Already-stored facts are kept.',
|
|
3106
|
-
};
|
|
3107
|
-
}
|
|
1917
|
+
configureImportRuntime({ storeExtractedFacts });
|
|
3108
1918
|
|
|
3109
1919
|
// ---------------------------------------------------------------------------
|
|
3110
1920
|
// buildRecallDeps — bind the real recall pipeline into the closures the
|
|
@@ -3379,9 +2189,9 @@ function buildRecallDeps(logger: OpenClawPluginApi['logger']): TrNativeMemoryDep
|
|
|
3379
2189
|
if (isSubgraphMode()) {
|
|
3380
2190
|
if (!subgraphOwner) return null;
|
|
3381
2191
|
try {
|
|
3382
|
-
const
|
|
3383
|
-
if (!
|
|
3384
|
-
const docJson = decryptFromHex(
|
|
2192
|
+
const fetchedFact = await fetchFactById(subgraphOwner, id, authKeyHex);
|
|
2193
|
+
if (!fetchedFact) return null;
|
|
2194
|
+
const docJson = decryptFromHex(fetchedFact.encryptedBlob, encryptionKey);
|
|
3385
2195
|
if (isDigestBlob(docJson)) return null;
|
|
3386
2196
|
const doc = readClaimFromBlob(docJson);
|
|
3387
2197
|
return { id, plaintext: doc.text };
|
|
@@ -3454,65 +2264,7 @@ const plugin = {
|
|
|
3454
2264
|
// `invalid config: must NOT have additional properties`, which blocked
|
|
3455
2265
|
// the documented remote-pairing setup (publicUrl) and made it impossible
|
|
3456
2266
|
// for a user to hand-pick an extraction model (extraction.llm.*).
|
|
3457
|
-
configSchema:
|
|
3458
|
-
type: 'object',
|
|
3459
|
-
additionalProperties: false,
|
|
3460
|
-
properties: {
|
|
3461
|
-
publicUrl: {
|
|
3462
|
-
type: 'string',
|
|
3463
|
-
description:
|
|
3464
|
-
"Public gateway URL for QR pairing (e.g. 'https://gateway.example.com:18789'). Overrides the auto-resolution cascade in buildPairingUrl.",
|
|
3465
|
-
},
|
|
3466
|
-
extraction: {
|
|
3467
|
-
type: 'object',
|
|
3468
|
-
additionalProperties: false,
|
|
3469
|
-
properties: {
|
|
3470
|
-
enabled: {
|
|
3471
|
-
type: 'boolean',
|
|
3472
|
-
description: 'Enable/disable auto-extraction (default: true)',
|
|
3473
|
-
},
|
|
3474
|
-
model: {
|
|
3475
|
-
type: 'string',
|
|
3476
|
-
description:
|
|
3477
|
-
"Shorthand: override just the extraction model (e.g., 'glm-4.5-flash', 'gpt-4.1-mini'). For a full provider override use extraction.llm.",
|
|
3478
|
-
},
|
|
3479
|
-
interval: {
|
|
3480
|
-
type: 'number',
|
|
3481
|
-
description: 'Number of turns between automatic extractions (default: 3)',
|
|
3482
|
-
},
|
|
3483
|
-
maxFactsPerExtraction: {
|
|
3484
|
-
type: 'number',
|
|
3485
|
-
description: 'Hard cap on facts extracted per turn (default: 15)',
|
|
3486
|
-
},
|
|
3487
|
-
llm: {
|
|
3488
|
-
type: 'object',
|
|
3489
|
-
additionalProperties: false,
|
|
3490
|
-
description:
|
|
3491
|
-
'Explicit LLM override block. Highest-priority tier in the extraction-provider cascade. Any subset of provider+apiKey is enough to pin a provider.',
|
|
3492
|
-
properties: {
|
|
3493
|
-
provider: {
|
|
3494
|
-
type: 'string',
|
|
3495
|
-
description:
|
|
3496
|
-
"Provider name: zai | openai | anthropic | gemini | google | mistral | groq | deepseek | openrouter | xai | together | cerebras.",
|
|
3497
|
-
},
|
|
3498
|
-
model: {
|
|
3499
|
-
type: 'string',
|
|
3500
|
-
description: 'Explicit model id. If omitted, deriveCheapModel(provider) picks a sensible default.',
|
|
3501
|
-
},
|
|
3502
|
-
apiKey: {
|
|
3503
|
-
type: 'string',
|
|
3504
|
-
description: 'API key for the selected provider. Required for the override to take effect.',
|
|
3505
|
-
},
|
|
3506
|
-
baseUrl: {
|
|
3507
|
-
type: 'string',
|
|
3508
|
-
description: 'Override the provider base URL (self-hosted / custom gateway setups).',
|
|
3509
|
-
},
|
|
3510
|
-
},
|
|
3511
|
-
},
|
|
3512
|
-
},
|
|
3513
|
-
},
|
|
3514
|
-
},
|
|
3515
|
-
},
|
|
2267
|
+
configSchema: CONFIG_SCHEMA,
|
|
3516
2268
|
|
|
3517
2269
|
register(api: OpenClawPluginApi) {
|
|
3518
2270
|
// NOTE: the body of register() below is intentionally NOT re-indent
|
|
@@ -3851,7 +2603,7 @@ const plugin = {
|
|
|
3851
2603
|
if (typeof api.registerCli === 'function') {
|
|
3852
2604
|
api.registerCli(
|
|
3853
2605
|
async ({ program }) => {
|
|
3854
|
-
const { registerOnboardingCli } = await import('./onboarding-cli.js');
|
|
2606
|
+
const { registerOnboardingCli } = await import('./pairing/onboarding-cli.js');
|
|
3855
2607
|
registerOnboardingCli(program as import('commander').Command, {
|
|
3856
2608
|
credentialsPath: CREDENTIALS_PATH,
|
|
3857
2609
|
statePath: CONFIG.onboardingStatePath,
|
|
@@ -3889,13 +2641,13 @@ const plugin = {
|
|
|
3889
2641
|
// same relay-brokered URL surface the agent tool uses. The local
|
|
3890
2642
|
// (gateway-loopback) flow is still available via `--local`. See
|
|
3891
2643
|
// pair-cli.ts header for the rationale.
|
|
3892
|
-
const { registerPairCli } = await import('./pair-cli.js');
|
|
2644
|
+
const { registerPairCli } = await import('./pairing/pair-cli.js');
|
|
3893
2645
|
registerPairCli(program as import('commander').Command, {
|
|
3894
2646
|
sessionsPath: CONFIG.pairSessionsPath,
|
|
3895
2647
|
renderPairingUrl: (session) => buildPairingUrl(api, session),
|
|
3896
2648
|
logger: api.logger,
|
|
3897
2649
|
runRelayPairCli: async (cliMode, runOpts) => {
|
|
3898
|
-
const { runRelayPairCli } = await import('./pair-cli-relay.js');
|
|
2650
|
+
const { runRelayPairCli } = await import('./pairing/pair-cli-relay.js');
|
|
3899
2651
|
return runRelayPairCli(cliMode, {
|
|
3900
2652
|
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
3901
2653
|
credentialsPath: CREDENTIALS_PATH,
|
|
@@ -3999,7 +2751,7 @@ const plugin = {
|
|
|
3999
2751
|
}) => {
|
|
4000
2752
|
try {
|
|
4001
2753
|
await requireFullSetup(api.logger);
|
|
4002
|
-
const
|
|
2754
|
+
const importResult = await handlePluginImportFrom({
|
|
4003
2755
|
source,
|
|
4004
2756
|
file_path: opts.file,
|
|
4005
2757
|
content: opts.content,
|
|
@@ -4012,30 +2764,30 @@ const plugin = {
|
|
|
4012
2764
|
}, api.logger);
|
|
4013
2765
|
|
|
4014
2766
|
if (opts.json) {
|
|
4015
|
-
process.stdout.write(JSON.stringify(
|
|
2767
|
+
process.stdout.write(JSON.stringify(importResult) + '\n');
|
|
4016
2768
|
} else {
|
|
4017
2769
|
// Human-readable summary. The handler already returns a
|
|
4018
2770
|
// `message` for chunked (background) imports; for direct
|
|
4019
2771
|
// stores + dry runs, synthesize a short summary.
|
|
4020
|
-
if (
|
|
4021
|
-
const chunks =
|
|
2772
|
+
if (importResult.dry_run) {
|
|
2773
|
+
const chunks = importResult.total_chunks as number | undefined;
|
|
4022
2774
|
if (chunks !== undefined) {
|
|
4023
2775
|
process.stdout.write(
|
|
4024
|
-
`Dry run: ~${
|
|
4025
|
-
`(~${
|
|
2776
|
+
`Dry run: ~${importResult.estimated_facts} facts from ${chunks} chunks ` +
|
|
2777
|
+
`(~${importResult.estimated_minutes} min). Confirm without --dry-run to start.\n`,
|
|
4026
2778
|
);
|
|
4027
2779
|
} else {
|
|
4028
2780
|
process.stdout.write(
|
|
4029
|
-
`Dry run: found ${
|
|
2781
|
+
`Dry run: found ${importResult.total_found} facts. Confirm without --dry-run to import.\n`,
|
|
4030
2782
|
);
|
|
4031
2783
|
}
|
|
4032
|
-
} else if (
|
|
2784
|
+
} else if (importResult.import_id && importResult.status === 'running') {
|
|
4033
2785
|
process.stdout.write(
|
|
4034
|
-
`${
|
|
2786
|
+
`${importResult.message}\nImport id: ${importResult.import_id}\n`,
|
|
4035
2787
|
);
|
|
4036
2788
|
} else {
|
|
4037
|
-
const stored =
|
|
4038
|
-
const total =
|
|
2789
|
+
const stored = importResult.imported as number | undefined;
|
|
2790
|
+
const total = importResult.total_found as number | undefined;
|
|
4039
2791
|
process.stdout.write(
|
|
4040
2792
|
`Imported ${stored ?? 0}/${total ?? stored ?? 0} facts from ${source}.\n`,
|
|
4041
2793
|
);
|
|
@@ -4060,25 +2812,25 @@ const plugin = {
|
|
|
4060
2812
|
.action(async (opts: { id?: string; json?: boolean }) => {
|
|
4061
2813
|
try {
|
|
4062
2814
|
await requireFullSetup(api.logger);
|
|
4063
|
-
const
|
|
2815
|
+
const statusResult = await handleImportStatus({ import_id: opts.id }, api.logger);
|
|
4064
2816
|
if (opts.json) {
|
|
4065
|
-
process.stdout.write(JSON.stringify(
|
|
2817
|
+
process.stdout.write(JSON.stringify(statusResult) + '\n');
|
|
4066
2818
|
} else {
|
|
4067
|
-
const status =
|
|
4068
|
-
const stored =
|
|
4069
|
-
const batchDone =
|
|
4070
|
-
const batchTotal =
|
|
2819
|
+
const status = statusResult.status as string | undefined;
|
|
2820
|
+
const stored = statusResult.facts_stored as number | undefined;
|
|
2821
|
+
const batchDone = statusResult.batch_done as number | undefined;
|
|
2822
|
+
const batchTotal = statusResult.batch_total as number | undefined;
|
|
4071
2823
|
if (status === 'no_active_import') {
|
|
4072
2824
|
process.stdout.write('No active import. Start one with `openclaw totalreclaw import from <source>`.\n');
|
|
4073
2825
|
} else if (status === 'running') {
|
|
4074
2826
|
process.stdout.write(
|
|
4075
|
-
`Import ${
|
|
2827
|
+
`Import ${statusResult.import_id}: running — ${stored} facts stored, ` +
|
|
4076
2828
|
`batch ${batchDone}/${batchTotal}` +
|
|
4077
|
-
(
|
|
2829
|
+
(statusResult.completion_iso ? `, ETA ${statusResult.completion_iso}` : '') + '.\n',
|
|
4078
2830
|
);
|
|
4079
2831
|
} else {
|
|
4080
2832
|
process.stdout.write(
|
|
4081
|
-
`Import ${
|
|
2833
|
+
`Import ${statusResult.import_id}: ${status} — ${stored ?? 0} facts stored.\n`,
|
|
4082
2834
|
);
|
|
4083
2835
|
}
|
|
4084
2836
|
}
|
|
@@ -4101,17 +2853,17 @@ const plugin = {
|
|
|
4101
2853
|
.action(async (importId: string, opts: { json?: boolean }) => {
|
|
4102
2854
|
try {
|
|
4103
2855
|
await requireFullSetup(api.logger);
|
|
4104
|
-
const
|
|
2856
|
+
const abortResult = await handleImportAbort({ import_id: importId }, api.logger);
|
|
4105
2857
|
if (opts.json) {
|
|
4106
|
-
process.stdout.write(JSON.stringify(
|
|
2858
|
+
process.stdout.write(JSON.stringify(abortResult) + '\n');
|
|
4107
2859
|
} else {
|
|
4108
|
-
if (
|
|
2860
|
+
if (abortResult.aborted) {
|
|
4109
2861
|
process.stdout.write(
|
|
4110
|
-
`Import ${importId}: abort requested. ${
|
|
2862
|
+
`Import ${importId}: abort requested. ${abortResult.facts_already_stored ?? 0} facts already stored (kept).\n`,
|
|
4111
2863
|
);
|
|
4112
2864
|
} else {
|
|
4113
2865
|
process.stdout.write(
|
|
4114
|
-
`Import ${importId}: ${
|
|
2866
|
+
`Import ${importId}: ${abortResult.error ?? 'abort failed'}\n`,
|
|
4115
2867
|
);
|
|
4116
2868
|
}
|
|
4117
2869
|
}
|
|
@@ -4158,15 +2910,15 @@ const plugin = {
|
|
|
4158
2910
|
throw new Error(`checkout session failed (HTTP ${response.status}): ${body || response.statusText}`);
|
|
4159
2911
|
}
|
|
4160
2912
|
|
|
4161
|
-
const
|
|
4162
|
-
if (!
|
|
2913
|
+
const checkoutJson = await response.json() as { checkout_url?: string };
|
|
2914
|
+
if (!checkoutJson.checkout_url) {
|
|
4163
2915
|
throw new Error('no checkout URL returned by the relay');
|
|
4164
2916
|
}
|
|
4165
2917
|
|
|
4166
2918
|
if (opts.json) {
|
|
4167
|
-
process.stdout.write(JSON.stringify({ checkout_url:
|
|
2919
|
+
process.stdout.write(JSON.stringify({ checkout_url: checkoutJson.checkout_url }) + '\n');
|
|
4168
2920
|
} else {
|
|
4169
|
-
process.stdout.write(`Open this URL to upgrade to Pro: ${
|
|
2921
|
+
process.stdout.write(`Open this URL to upgrade to Pro: ${checkoutJson.checkout_url}\n`);
|
|
4170
2922
|
}
|
|
4171
2923
|
} catch (err: unknown) {
|
|
4172
2924
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -5169,8 +3921,8 @@ const plugin = {
|
|
|
5169
3921
|
|
|
5170
3922
|
// BUG-2 fix: skip extraction if an import was in progress this turn.
|
|
5171
3923
|
// Import failures were retriggering agent_end → extraction → import loops.
|
|
5172
|
-
if (
|
|
5173
|
-
|
|
3924
|
+
if (isImportInProgress()) {
|
|
3925
|
+
setImportInProgress(false); // auto-reset for next turn
|
|
5174
3926
|
api.logger.info('agent_end: skipping extraction (import was in progress)');
|
|
5175
3927
|
return { memoryHandled: true };
|
|
5176
3928
|
}
|
|
@@ -5278,7 +4030,7 @@ const plugin = {
|
|
|
5278
4030
|
logger: api.logger,
|
|
5279
4031
|
ensureInitialized: () => ensureInitialized(api.logger),
|
|
5280
4032
|
isPairingPending: () => needsSetup,
|
|
5281
|
-
isImportActive: () =>
|
|
4033
|
+
isImportActive: () => isImportInProgress(),
|
|
5282
4034
|
getExtractInterval,
|
|
5283
4035
|
getMaxFactsPerExtraction,
|
|
5284
4036
|
isDedupEnabled: isLlmDedupEnabled,
|