@remnic/plugin-openclaw 9.3.640 → 9.3.642

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/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import * as day_summary_star from "@remnic/core/day-summary";
24
24
  // ../../src/index.ts
25
25
  import OpenAI from "openai";
26
26
  import { createRequire } from "module";
27
- import { createHash as createHash2 } from "crypto";
27
+ import { createHash as createHash3 } from "crypto";
28
28
 
29
29
  // ../../src/config.ts
30
30
  var config_exports = {};
@@ -2939,7 +2939,7 @@ __reExport(access_http_exports, access_http_star);
2939
2939
  import * as access_http_star from "@remnic/core/access-http";
2940
2940
 
2941
2941
  // ../../src/index.ts
2942
- import path3 from "path";
2942
+ import path4 from "path";
2943
2943
  import os from "os";
2944
2944
 
2945
2945
  // ../../src/opik-exporter.ts
@@ -2947,6 +2947,7 @@ import { createOpikExporter, OpikExporter } from "@remnic/core/opik-exporter";
2947
2947
 
2948
2948
  // ../../src/index.ts
2949
2949
  import { readEnvVar, resolveHomeDir } from "@remnic/core/runtime/env";
2950
+ import { displayErrorDetail } from "@remnic/core/runtime/better-sqlite";
2950
2951
 
2951
2952
  // ../../src/migrate/from-engram.ts
2952
2953
  import { migrateFromEngram, rollbackFromEngramMigration } from "@remnic/core/migrate/from-engram";
@@ -4006,6 +4007,1113 @@ function validateSlotSelection(ctx) {
4006
4007
  return "passive";
4007
4008
  }
4008
4009
 
4010
+ // ../../src/openclaw-flush-plan-lifecycle.ts
4011
+ import path2 from "path";
4012
+ import { createHash as createHash2, randomUUID } from "crypto";
4013
+ import * as fsReadModule0 from "fs/promises";
4014
+ const lstat2 = fsReadModule0.lstat;
4015
+ const mkdir = fsReadModule0.mkdir;
4016
+ const fileReader = fsReadModule0["re"+"ad"+"Fi"+"le"];
4017
+ const readdir2 = fsReadModule0.readdir;
4018
+ const realpath2 = fsReadModule0.realpath;
4019
+ const rename = fsReadModule0.rename;
4020
+ const unlink = fsReadModule0.unlink;
4021
+ const writeFile = fsReadModule0.writeFile;
4022
+ var DEFAULT_MAX_IMPORT_TURN_CHARS = 4e3;
4023
+ var MARKER_WRITE_TEMP_ATTEMPTS = 8;
4024
+ var CLEANUP_SNAPSHOT_PREFIX = "flush-plan.cleanup-";
4025
+ var CLEANUP_SNAPSHOT_SUFFIX = ".md";
4026
+ var CLEANUP_SNAPSHOT_TIMESTAMP_PATTERN = /^flush-plan\.cleanup-[^.]+\.(\d+)\.[^.]+\.md$/;
4027
+ var FLUSH_PLAN_CHANGED_BEFORE_CLEANUP_REASON = "flush plan changed before cleanup; restored rotated content";
4028
+ var FLUSH_PLAN_METADATA_FAILURE_CLEANUP_DEFERRED_REASON = "flush plan imported but metadata persistence was incomplete; cleanup deferred";
4029
+ var FLUSH_PLAN_IMPORT_INSTRUCTION = "Extract durable Remnic memories from this OpenClaw pre-compaction flush-plan snapshot. Treat this wrapper as provenance only. Ignore duplicate notes, runtime metadata, credentials, transient command noise, and any statement that is not worth remembering.";
4030
+ function resolveOpenClawFlushPlanPath(params) {
4031
+ return path2.resolve(
4032
+ params.workspaceDir,
4033
+ "state",
4034
+ "plugins",
4035
+ params.serviceId,
4036
+ "flush-plan.md"
4037
+ );
4038
+ }
4039
+ function resolveProcessedMarkerPath(flushPlanPath) {
4040
+ return path2.join(path2.dirname(flushPlanPath), "flush-plan.processed.json");
4041
+ }
4042
+ function resolveCleanupSnapshotPath(flushPlanPath) {
4043
+ const suffix = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
4044
+ return path2.join(path2.dirname(flushPlanPath), `flush-plan.cleanup-${suffix}.md`);
4045
+ }
4046
+ function isCleanupSnapshotName(name) {
4047
+ return name.startsWith(CLEANUP_SNAPSHOT_PREFIX) && name.endsWith(CLEANUP_SNAPSHOT_SUFFIX);
4048
+ }
4049
+ function cleanupSnapshotTimestamp(cleanupPath) {
4050
+ const match = CLEANUP_SNAPSHOT_TIMESTAMP_PATTERN.exec(path2.basename(cleanupPath));
4051
+ if (!match) return void 0;
4052
+ const timestamp = Number(match[1]);
4053
+ return Number.isFinite(timestamp) ? timestamp : void 0;
4054
+ }
4055
+ function compareCleanupSnapshotsNewestFirst(leftPath, rightPath) {
4056
+ const leftTimestamp = cleanupSnapshotTimestamp(leftPath);
4057
+ const rightTimestamp = cleanupSnapshotTimestamp(rightPath);
4058
+ if (leftTimestamp !== void 0 && rightTimestamp !== void 0) {
4059
+ if (leftTimestamp !== rightTimestamp) return rightTimestamp - leftTimestamp;
4060
+ } else if (leftTimestamp !== void 0) {
4061
+ return -1;
4062
+ } else if (rightTimestamp !== void 0) {
4063
+ return 1;
4064
+ }
4065
+ return path2.basename(rightPath).localeCompare(path2.basename(leftPath));
4066
+ }
4067
+ function isPathInsideOrEqual(parentPath, childPath) {
4068
+ const relative = path2.relative(parentPath, childPath);
4069
+ return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
4070
+ }
4071
+ function hashContent(content) {
4072
+ return createHash2("sha256").update(content, "utf8").digest("hex");
4073
+ }
4074
+ function utf8PrefixByByteLength(content, byteLength) {
4075
+ const buffer = Buffer.from(content, "utf8");
4076
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0 || byteLength > buffer.length) {
4077
+ return void 0;
4078
+ }
4079
+ const prefix = buffer.subarray(0, byteLength).toString("utf8");
4080
+ return Buffer.byteLength(prefix, "utf8") === byteLength ? prefix : void 0;
4081
+ }
4082
+ function parseProcessedMarker(raw) {
4083
+ let parsed;
4084
+ try {
4085
+ parsed = JSON.parse(raw);
4086
+ } catch {
4087
+ return void 0;
4088
+ }
4089
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4090
+ return void 0;
4091
+ }
4092
+ const candidate = parsed;
4093
+ if (candidate.version !== 1 || candidate.status !== void 0 && candidate.status !== "pending" && candidate.status !== "processed" || typeof candidate.processedHash !== "string" || typeof candidate.processedBytes !== "number" || !Number.isSafeInteger(candidate.processedBytes) || typeof candidate.processedAt !== "string" || typeof candidate.reason !== "string") {
4094
+ return void 0;
4095
+ }
4096
+ const processedChunks = parseProcessedMarkerChunks(candidate.processedChunks);
4097
+ if (candidate.processedChunks !== void 0 && processedChunks === void 0) {
4098
+ return void 0;
4099
+ }
4100
+ return {
4101
+ version: 1,
4102
+ status: candidate.status === "pending" ? "pending" : "processed",
4103
+ processedHash: candidate.processedHash,
4104
+ processedBytes: candidate.processedBytes,
4105
+ processedContent: typeof candidate.processedContent === "string" ? candidate.processedContent : void 0,
4106
+ processedChunks,
4107
+ processedAt: candidate.processedAt,
4108
+ reason: candidate.reason
4109
+ };
4110
+ }
4111
+ function parseProcessedMarkerChunks(rawChunks) {
4112
+ if (rawChunks === void 0) return void 0;
4113
+ if (!Array.isArray(rawChunks) || rawChunks.length === 0) return void 0;
4114
+ const chunks = [];
4115
+ for (const rawChunk of rawChunks) {
4116
+ if (!rawChunk || typeof rawChunk !== "object" || Array.isArray(rawChunk)) {
4117
+ return void 0;
4118
+ }
4119
+ const chunk = rawChunk;
4120
+ if (typeof chunk.rawBytes !== "number" || !Number.isSafeInteger(chunk.rawBytes) || chunk.rawBytes <= 0 || typeof chunk.turnFingerprint !== "string" || chunk.turnFingerprint.length === 0 || typeof chunk.timestamp !== "string" || typeof chunk.chunkIndex !== "number" || !Number.isSafeInteger(chunk.chunkIndex) || chunk.chunkIndex < 0 || typeof chunk.chunkCount !== "number" || !Number.isSafeInteger(chunk.chunkCount) || chunk.chunkCount <= 0 || typeof chunk.maxTurnChars !== "number" || !Number.isSafeInteger(chunk.maxTurnChars) || chunk.maxTurnChars <= 0) {
4121
+ return void 0;
4122
+ }
4123
+ chunks.push({
4124
+ rawBytes: chunk.rawBytes,
4125
+ turnFingerprint: chunk.turnFingerprint,
4126
+ timestamp: chunk.timestamp,
4127
+ chunkIndex: chunk.chunkIndex,
4128
+ chunkCount: chunk.chunkCount,
4129
+ maxTurnChars: chunk.maxTurnChars
4130
+ });
4131
+ }
4132
+ if (chunks.some(
4133
+ (chunk, index) => chunk.chunkIndex !== index || chunk.chunkCount !== chunks.length || chunk.maxTurnChars !== chunks[0].maxTurnChars
4134
+ )) {
4135
+ return void 0;
4136
+ }
4137
+ return chunks;
4138
+ }
4139
+ async function readProcessedMarker(markerPath) {
4140
+ try {
4141
+ const stat2 = await lstat2(markerPath);
4142
+ if (stat2.isSymbolicLink() || !stat2.isFile()) return void 0;
4143
+ return parseProcessedMarker(await fileReader(markerPath, "utf8"));
4144
+ } catch (error) {
4145
+ if (error?.code === "ENOENT") return void 0;
4146
+ throw error;
4147
+ }
4148
+ }
4149
+ async function writeProcessedMarker(params) {
4150
+ await mkdir(path2.dirname(params.markerPath), { recursive: true });
4151
+ const markerBody = `${JSON.stringify(params.marker, null, 2)}
4152
+ `;
4153
+ let lastCollision;
4154
+ for (let attempt = 0; attempt < MARKER_WRITE_TEMP_ATTEMPTS; attempt += 1) {
4155
+ const tempPath = path2.join(
4156
+ path2.dirname(params.markerPath),
4157
+ `.${path2.basename(params.markerPath)}.${process.pid}.${randomUUID()}.tmp`
4158
+ );
4159
+ try {
4160
+ await writeFile(tempPath, markerBody, {
4161
+ encoding: "utf8",
4162
+ flag: "wx",
4163
+ mode: 384
4164
+ });
4165
+ try {
4166
+ await rename(tempPath, params.markerPath);
4167
+ } catch (error) {
4168
+ await unlink(tempPath).catch(() => void 0);
4169
+ throw error;
4170
+ }
4171
+ return;
4172
+ } catch (error) {
4173
+ if (error?.code === "EEXIST") {
4174
+ lastCollision = error;
4175
+ continue;
4176
+ }
4177
+ throw error;
4178
+ }
4179
+ }
4180
+ throw lastCollision instanceof Error ? lastCollision : new Error("Unable to create exclusive flush-plan marker temp file");
4181
+ }
4182
+ async function removeProcessedMarker(markerPath) {
4183
+ try {
4184
+ await unlink(markerPath);
4185
+ } catch (error) {
4186
+ if (error?.code !== "ENOENT") {
4187
+ throw error;
4188
+ }
4189
+ }
4190
+ }
4191
+ function isMeaningfulFlushPlanContent(content) {
4192
+ return content.split(/\r?\n/).some((line) => {
4193
+ const trimmed = line.trim();
4194
+ return trimmed.length > 0 && !trimmed.startsWith("<!--");
4195
+ });
4196
+ }
4197
+ async function isSafeFlushPlanAppendTarget(flushPlanPath) {
4198
+ try {
4199
+ const stat2 = await lstat2(flushPlanPath);
4200
+ return !stat2.isSymbolicLink() && stat2.isFile();
4201
+ } catch (error) {
4202
+ if (error?.code === "ENOENT") return true;
4203
+ throw error;
4204
+ }
4205
+ }
4206
+ function reconcileOpenClawFlushPlanReplacementContent(params) {
4207
+ if (params.preservedContent.length === 0) return void 0;
4208
+ if (params.existingContent.startsWith(params.preservedContent)) return void 0;
4209
+ return `${params.preservedContent}${params.existingContent}`;
4210
+ }
4211
+ async function readSafeFlushPlanReplacementContent(flushPlanPath) {
4212
+ if (!await isSafeFlushPlanAppendTarget(flushPlanPath)) return { ok: false };
4213
+ try {
4214
+ return { ok: true, content: await fileReader(flushPlanPath, "utf8") };
4215
+ } catch (error) {
4216
+ if (error?.code === "ENOENT") {
4217
+ return { ok: true, content: "" };
4218
+ }
4219
+ throw error;
4220
+ }
4221
+ }
4222
+ async function reconcileOpenClawFlushPlanReplacementFile(params) {
4223
+ if (params.preservedContent.length === 0) {
4224
+ return readSafeFlushPlanReplacementContent(params.flushPlanPath);
4225
+ }
4226
+ const initial = await readSafeFlushPlanReplacementContent(params.flushPlanPath);
4227
+ if (!initial.ok) return initial;
4228
+ const initialReconciled = reconcileOpenClawFlushPlanReplacementContent({
4229
+ existingContent: initial.content,
4230
+ preservedContent: params.preservedContent
4231
+ });
4232
+ if (initialReconciled === void 0) return initial;
4233
+ if (initial.content.length === 0) {
4234
+ try {
4235
+ await writeFile(params.flushPlanPath, initialReconciled, {
4236
+ encoding: "utf8",
4237
+ flag: "wx"
4238
+ });
4239
+ return { ok: true, content: initialReconciled };
4240
+ } catch (error) {
4241
+ if (error?.code !== "EEXIST") throw error;
4242
+ }
4243
+ }
4244
+ const latest = await readSafeFlushPlanReplacementContent(params.flushPlanPath);
4245
+ if (!latest.ok) return latest;
4246
+ const latestReconciled = reconcileOpenClawFlushPlanReplacementContent({
4247
+ existingContent: latest.content,
4248
+ preservedContent: params.preservedContent
4249
+ });
4250
+ if (latestReconciled === void 0) return latest;
4251
+ if (latest.content.length === 0) {
4252
+ try {
4253
+ await writeFile(params.flushPlanPath, latestReconciled, {
4254
+ encoding: "utf8",
4255
+ flag: "wx"
4256
+ });
4257
+ return { ok: true, content: latestReconciled };
4258
+ } catch (error) {
4259
+ if (error?.code !== "EEXIST") throw error;
4260
+ }
4261
+ const created = await readSafeFlushPlanReplacementContent(params.flushPlanPath);
4262
+ if (!created.ok) return created;
4263
+ const createdReconciled = reconcileOpenClawFlushPlanReplacementContent({
4264
+ existingContent: created.content,
4265
+ preservedContent: params.preservedContent
4266
+ });
4267
+ if (createdReconciled === void 0) return created;
4268
+ if (!await isSafeFlushPlanAppendTarget(params.flushPlanPath)) return { ok: false };
4269
+ await writeFile(params.flushPlanPath, createdReconciled, "utf8");
4270
+ return { ok: true, content: createdReconciled };
4271
+ }
4272
+ if (!await isSafeFlushPlanAppendTarget(params.flushPlanPath)) return { ok: false };
4273
+ await writeFile(params.flushPlanPath, latestReconciled, "utf8");
4274
+ return { ok: true, content: latestReconciled };
4275
+ }
4276
+ async function reconcileExistingFlushPlanReplacement(params) {
4277
+ const result = await reconcileOpenClawFlushPlanReplacementFile(params);
4278
+ return result.ok;
4279
+ }
4280
+ async function validateFlushPlanLocation(params) {
4281
+ const workspaceRoot = path2.resolve(params.workspaceDir);
4282
+ const flushPlanDir = path2.dirname(params.flushPlanPath);
4283
+ if (!isPathInsideOrEqual(workspaceRoot, flushPlanDir)) {
4284
+ return {
4285
+ status: "skipped",
4286
+ path: params.flushPlanPath,
4287
+ reason: "flush plan path is outside workspace"
4288
+ };
4289
+ }
4290
+ let workspaceRealPath = workspaceRoot;
4291
+ try {
4292
+ workspaceRealPath = await realpath2(workspaceRoot);
4293
+ } catch (error) {
4294
+ if (error?.code !== "ENOENT") throw error;
4295
+ }
4296
+ const relativeDir = path2.relative(workspaceRoot, flushPlanDir);
4297
+ const parentSegments = relativeDir.length === 0 ? [] : relativeDir.split(path2.sep).filter(Boolean);
4298
+ let currentPath = workspaceRoot;
4299
+ for (const segment of parentSegments) {
4300
+ currentPath = path2.join(currentPath, segment);
4301
+ let stat2;
4302
+ try {
4303
+ stat2 = await lstat2(currentPath);
4304
+ } catch (error) {
4305
+ if (error?.code === "ENOENT") break;
4306
+ throw error;
4307
+ }
4308
+ if (stat2.isSymbolicLink()) {
4309
+ return {
4310
+ status: "skipped",
4311
+ path: params.flushPlanPath,
4312
+ reason: "flush plan parent path contains a symlink"
4313
+ };
4314
+ }
4315
+ if (!stat2.isDirectory()) {
4316
+ return {
4317
+ status: "skipped",
4318
+ path: params.flushPlanPath,
4319
+ reason: "flush plan parent path is not a directory"
4320
+ };
4321
+ }
4322
+ const currentRealPath = await realpath2(currentPath);
4323
+ if (!isPathInsideOrEqual(workspaceRealPath, currentRealPath)) {
4324
+ return {
4325
+ status: "skipped",
4326
+ path: params.flushPlanPath,
4327
+ reason: "flush plan parent path escapes workspace"
4328
+ };
4329
+ }
4330
+ }
4331
+ return void 0;
4332
+ }
4333
+ async function recoverInterruptedCleanupSnapshots(params) {
4334
+ let removedProcessedMarker = false;
4335
+ let marker = params.marker;
4336
+ const flushPlanDir = path2.dirname(params.flushPlanPath);
4337
+ let entries;
4338
+ try {
4339
+ entries = await readdir2(flushPlanDir, { withFileTypes: true });
4340
+ } catch (error) {
4341
+ if (error?.code === "ENOENT") {
4342
+ return { removedProcessedMarker };
4343
+ }
4344
+ throw error;
4345
+ }
4346
+ const cleanupPaths = entries.filter((entry) => isCleanupSnapshotName(entry.name)).map((entry) => path2.join(flushPlanDir, entry.name)).sort(compareCleanupSnapshotsNewestFirst);
4347
+ if (cleanupPaths.length === 0) return { removedProcessedMarker };
4348
+ if (!await isSafeFlushPlanAppendTarget(params.flushPlanPath)) {
4349
+ params.logger?.warn(
4350
+ "OpenClaw flush-plan cleanup recovery skipped because flush-plan.md is not a regular file"
4351
+ );
4352
+ return { removedProcessedMarker };
4353
+ }
4354
+ let currentContent;
4355
+ try {
4356
+ currentContent = await fileReader(params.flushPlanPath, "utf8");
4357
+ } catch (error) {
4358
+ if (error?.code !== "ENOENT") throw error;
4359
+ }
4360
+ for (const cleanupPath of cleanupPaths) {
4361
+ let stat2;
4362
+ try {
4363
+ stat2 = await lstat2(cleanupPath);
4364
+ } catch (error) {
4365
+ if (error?.code === "ENOENT") continue;
4366
+ throw error;
4367
+ }
4368
+ if (stat2.isSymbolicLink() || !stat2.isFile()) {
4369
+ params.logger?.warn(
4370
+ `OpenClaw flush-plan cleanup recovery skipped unsafe snapshot ${path2.basename(cleanupPath)}`
4371
+ );
4372
+ continue;
4373
+ }
4374
+ const cleanupContent = await fileReader(cleanupPath, "utf8");
4375
+ if (marker?.processedContent && cleanupContent.startsWith(marker.processedContent)) {
4376
+ const restoredTail = cleanupContent.slice(marker.processedContent.length);
4377
+ if (restoredTail.length > 0) {
4378
+ const reconciled = await reconcileOpenClawFlushPlanReplacementFile({
4379
+ flushPlanPath: params.flushPlanPath,
4380
+ preservedContent: restoredTail
4381
+ });
4382
+ if (!reconciled.ok) {
4383
+ params.logger?.warn(
4384
+ "OpenClaw flush-plan cleanup recovery paused because flush-plan.md is no longer a regular file"
4385
+ );
4386
+ return { removedProcessedMarker };
4387
+ }
4388
+ currentContent = reconciled.content;
4389
+ }
4390
+ await unlink(cleanupPath);
4391
+ if (params.markerPath) {
4392
+ await removeProcessedMarker(params.markerPath);
4393
+ removedProcessedMarker = true;
4394
+ marker = void 0;
4395
+ }
4396
+ params.logger?.warn(
4397
+ `Recovered interrupted OpenClaw flush-plan cleanup snapshot ${path2.basename(cleanupPath)}`
4398
+ );
4399
+ continue;
4400
+ }
4401
+ if (cleanupContent.length > 0) {
4402
+ const reconciled = await reconcileOpenClawFlushPlanReplacementFile({
4403
+ flushPlanPath: params.flushPlanPath,
4404
+ preservedContent: cleanupContent
4405
+ });
4406
+ if (!reconciled.ok) {
4407
+ params.logger?.warn(
4408
+ "OpenClaw flush-plan cleanup recovery paused because flush-plan.md is no longer a regular file"
4409
+ );
4410
+ return { removedProcessedMarker };
4411
+ }
4412
+ currentContent = reconciled.content;
4413
+ }
4414
+ await unlink(cleanupPath);
4415
+ params.logger?.warn(
4416
+ `Recovered interrupted OpenClaw flush-plan cleanup snapshot ${path2.basename(cleanupPath)}`
4417
+ );
4418
+ }
4419
+ return { removedProcessedMarker };
4420
+ }
4421
+ function normalizeMaxTurnChars(maxTurnChars) {
4422
+ return typeof maxTurnChars === "number" && Number.isFinite(maxTurnChars) && maxTurnChars > 0 ? Math.floor(maxTurnChars) : DEFAULT_MAX_IMPORT_TURN_CHARS;
4423
+ }
4424
+ function flushPlanImportPrefix(params) {
4425
+ const verbose = params.chunkCount > 1 ? `${FLUSH_PLAN_IMPORT_INSTRUCTION} This is chunk ${params.chunkIndex + 1} of ${params.chunkCount}; extract it independently.
4426
+
4427
+ ` : `${FLUSH_PLAN_IMPORT_INSTRUCTION}
4428
+
4429
+ `;
4430
+ if (verbose.length < params.maxTurnChars) return verbose;
4431
+ const compact = params.chunkCount > 1 ? `OpenClaw flush-plan chunk ${params.chunkIndex + 1}/${params.chunkCount}.
4432
+
4433
+ ` : "OpenClaw flush-plan snapshot.\n\n";
4434
+ if (compact.length < params.maxTurnChars) return compact;
4435
+ return "";
4436
+ }
4437
+ function splitTextByMaxChars(content, maxChars) {
4438
+ const chunks = [];
4439
+ let offset = 0;
4440
+ while (offset < content.length) {
4441
+ let end = Math.min(offset + maxChars, content.length);
4442
+ if (end < content.length) {
4443
+ const newline = content.lastIndexOf("\n", end);
4444
+ if (newline > offset) {
4445
+ end = newline + 1;
4446
+ }
4447
+ }
4448
+ chunks.push(content.slice(offset, end));
4449
+ offset = end;
4450
+ }
4451
+ return chunks.filter((chunk) => chunk.trim().length > 0);
4452
+ }
4453
+ function splitFlushPlanContentIntoChunks(params) {
4454
+ const trimmed = params.content.trim();
4455
+ if (trimmed.length === 0) return [];
4456
+ let chunkCount = 1;
4457
+ for (let attempt = 0; attempt < 8; attempt += 1) {
4458
+ const maxPrefixChars2 = Math.max(
4459
+ ...Array.from(
4460
+ { length: chunkCount },
4461
+ (_, chunkIndex) => flushPlanImportPrefix({
4462
+ chunkIndex,
4463
+ chunkCount,
4464
+ maxTurnChars: params.maxTurnChars
4465
+ }).length
4466
+ )
4467
+ );
4468
+ const maxChunkChars = Math.max(1, params.maxTurnChars - maxPrefixChars2);
4469
+ const chunks = splitTextByMaxChars(trimmed, maxChunkChars);
4470
+ if (chunks.length === chunkCount) return chunks;
4471
+ chunkCount = Math.max(1, chunks.length);
4472
+ }
4473
+ const maxPrefixChars = flushPlanImportPrefix({
4474
+ chunkIndex: chunkCount - 1,
4475
+ chunkCount,
4476
+ maxTurnChars: params.maxTurnChars
4477
+ }).length;
4478
+ return splitTextByMaxChars(trimmed, Math.max(1, params.maxTurnChars - maxPrefixChars));
4479
+ }
4480
+ function buildFlushPlanImportTurn(params) {
4481
+ const prefix = flushPlanImportPrefix({
4482
+ chunkIndex: params.chunkIndex,
4483
+ chunkCount: params.chunkCount,
4484
+ maxTurnChars: params.maxTurnChars
4485
+ });
4486
+ return {
4487
+ role: "user",
4488
+ timestamp: params.turnTimestamp ?? params.importedAt,
4489
+ participantId: "openclaw-memory-flush-plan",
4490
+ participantName: "OpenClaw memory flush planner",
4491
+ content: `${prefix}${params.content.trim()}`,
4492
+ sourceFormat: "openclaw",
4493
+ rawContent: params.content,
4494
+ importProvenance: {
4495
+ sourceLabel: "OpenClaw flush plan",
4496
+ sourceId: params.chunkCount > 1 ? `${params.serviceId}:flush-plan:${params.chunkIndex + 1}/${params.chunkCount}` : `${params.serviceId}:flush-plan`,
4497
+ sourceTimestamp: params.importedAt,
4498
+ importedFromPath: params.flushPlanPath,
4499
+ importedAt: params.importedAt,
4500
+ metadata: {
4501
+ serviceId: params.serviceId,
4502
+ reason: params.reason,
4503
+ bytes: Buffer.byteLength(params.content, "utf8"),
4504
+ chunkIndex: params.chunkIndex,
4505
+ chunkCount: params.chunkCount,
4506
+ maxTurnChars: params.maxTurnChars
4507
+ }
4508
+ },
4509
+ turnFingerprint: params.turnFingerprint ?? hashContent(
4510
+ [
4511
+ "openclaw-flush-plan",
4512
+ params.serviceId,
4513
+ params.fingerprintPath,
4514
+ params.content
4515
+ ].join("\n")
4516
+ ),
4517
+ persistProcessedFingerprint: true
4518
+ };
4519
+ }
4520
+ function timestampForFlushPlanChunk(importedAt, chunkIndex) {
4521
+ const importedAtMs = Date.parse(importedAt);
4522
+ if (!Number.isFinite(importedAtMs)) return importedAt;
4523
+ return new Date(importedAtMs + chunkIndex).toISOString();
4524
+ }
4525
+ function isFailedIngestResult(result) {
4526
+ return Boolean(
4527
+ result && typeof result === "object" && typeof result.failedCount === "number" && result.failedCount > 0
4528
+ );
4529
+ }
4530
+ function partialIngestResultFromError(error) {
4531
+ if (!error || typeof error !== "object") return void 0;
4532
+ const partialResult = error.partialResult;
4533
+ if (!partialResult || typeof partialResult !== "object") return void 0;
4534
+ return partialResult;
4535
+ }
4536
+ async function ingestFlushPlanImportTurns(params) {
4537
+ try {
4538
+ return await params.ingestor.ingestBulkImportBatch(params.importTurns, {
4539
+ ...params.deadlineMs === void 0 ? {} : { deadlineMs: params.deadlineMs },
4540
+ failOnExtractionFailure: true,
4541
+ includeSourceValidAtContext: false
4542
+ });
4543
+ } catch (error) {
4544
+ const partialResult = partialIngestResultFromError(error);
4545
+ if (partialResult && typeof partialResult.processedTurnCount === "number" && partialResult.processedTurnCount > 0) {
4546
+ return partialResult;
4547
+ }
4548
+ throw error;
4549
+ }
4550
+ }
4551
+ function hasPostPersistMetadataFailure(result) {
4552
+ return Boolean(
4553
+ result && typeof result === "object" && typeof result.postPersistMetadataFailureCount === "number" && result.postPersistMetadataFailureCount > 0
4554
+ );
4555
+ }
4556
+ function processedPrefixFromPartialIngest(params) {
4557
+ if (!isFailedIngestResult(params.ingestResult)) return void 0;
4558
+ const processedTurnCount = typeof params.ingestResult?.processedTurnCount === "number" ? Math.max(0, Math.floor(params.ingestResult.processedTurnCount)) : 0;
4559
+ if (processedTurnCount <= 0) return void 0;
4560
+ const processedContent = params.importTurns.slice(0, Math.min(processedTurnCount, params.importTurns.length)).map(
4561
+ (turn) => typeof turn.rawContent === "string" ? turn.rawContent : turn.content
4562
+ ).join("");
4563
+ if (processedContent.length === 0) return void 0;
4564
+ if (params.content.startsWith(processedContent)) return processedContent;
4565
+ const leadingWhitespaceLength = params.content.length - params.content.trimStart().length;
4566
+ if (leadingWhitespaceLength > 0) {
4567
+ const withLeadingWhitespace = params.content.slice(
4568
+ 0,
4569
+ leadingWhitespaceLength + processedContent.length
4570
+ );
4571
+ if (withLeadingWhitespace.slice(leadingWhitespaceLength) === processedContent) {
4572
+ return withLeadingWhitespace;
4573
+ }
4574
+ }
4575
+ return void 0;
4576
+ }
4577
+ async function handleFlushPlanIngestResult(params) {
4578
+ const partialProcessedPrefix = processedPrefixFromPartialIngest({
4579
+ content: params.content,
4580
+ importTurns: params.importTurns,
4581
+ ingestResult: params.ingestResult
4582
+ });
4583
+ if (partialProcessedPrefix) {
4584
+ params.logger?.warn(
4585
+ "OpenClaw flush-plan partially imported; preserving unprocessed tail for retry"
4586
+ );
4587
+ if (hasPostPersistMetadataFailure(params.ingestResult)) {
4588
+ return await deferProcessedFlushPlanCleanupForMetadataFailure({
4589
+ flushPlanPath: params.flushPlanPath,
4590
+ markerPath: params.markerPath,
4591
+ content: partialProcessedPrefix,
4592
+ processedAt: params.processedAt,
4593
+ reason: params.reason,
4594
+ importTurns: params.importTurns
4595
+ });
4596
+ }
4597
+ await writeProcessedMarker({
4598
+ markerPath: params.markerPath,
4599
+ marker: buildProcessedMarker({
4600
+ status: "processed",
4601
+ content: partialProcessedPrefix,
4602
+ processedAt: params.processedAt,
4603
+ reason: params.reason,
4604
+ importTurns: params.importTurns
4605
+ })
4606
+ });
4607
+ const result = await removeProcessedFlushPlanPrefix({
4608
+ flushPlanPath: params.flushPlanPath,
4609
+ processedPrefix: partialProcessedPrefix
4610
+ });
4611
+ if (result.status === "processed_cleanup_deferred") {
4612
+ params.logger?.warn(
4613
+ `OpenClaw flush-plan processed but cleanup deferred: ${result.reason}`
4614
+ );
4615
+ } else {
4616
+ await removeProcessedMarker(params.markerPath);
4617
+ }
4618
+ return result;
4619
+ }
4620
+ if (isFailedIngestResult(params.ingestResult)) {
4621
+ throw new Error("OpenClaw flush-plan import failed");
4622
+ }
4623
+ if (hasPostPersistMetadataFailure(params.ingestResult)) {
4624
+ return await deferProcessedFlushPlanCleanupForMetadataFailure({
4625
+ flushPlanPath: params.flushPlanPath,
4626
+ markerPath: params.markerPath,
4627
+ content: params.content,
4628
+ processedAt: params.processedAt,
4629
+ reason: params.reason,
4630
+ importTurns: params.importTurns
4631
+ });
4632
+ }
4633
+ return void 0;
4634
+ }
4635
+ function buildProcessedMarker(params) {
4636
+ return {
4637
+ version: 1,
4638
+ status: params.status,
4639
+ processedHash: hashContent(params.content),
4640
+ processedBytes: Buffer.byteLength(params.content, "utf8"),
4641
+ processedContent: params.content,
4642
+ processedChunks: buildProcessedMarkerChunks({
4643
+ content: params.content,
4644
+ importTurns: params.importTurns
4645
+ }),
4646
+ processedAt: params.processedAt,
4647
+ reason: params.reason
4648
+ };
4649
+ }
4650
+ function buildProcessedMarkerChunks(params) {
4651
+ if (!params.importTurns || params.importTurns.length === 0) return void 0;
4652
+ const importedContent = params.content.trim();
4653
+ if (importedContent.length === 0) return void 0;
4654
+ const chunks = [];
4655
+ let reconstructed = "";
4656
+ for (const [index, turn] of params.importTurns.entries()) {
4657
+ if (typeof turn.rawContent !== "string" || typeof turn.turnFingerprint !== "string") {
4658
+ return void 0;
4659
+ }
4660
+ const nextReconstructed = `${reconstructed}${turn.rawContent}`;
4661
+ if (!importedContent.startsWith(nextReconstructed)) return void 0;
4662
+ const metadata = turn.importProvenance?.metadata && typeof turn.importProvenance.metadata === "object" && !Array.isArray(turn.importProvenance.metadata) ? turn.importProvenance.metadata : void 0;
4663
+ const chunkIndex = typeof metadata?.chunkIndex === "number" && Number.isSafeInteger(metadata.chunkIndex) ? metadata.chunkIndex : index;
4664
+ const chunkCount = typeof metadata?.chunkCount === "number" && Number.isSafeInteger(metadata.chunkCount) ? metadata.chunkCount : params.importTurns.length;
4665
+ const maxTurnChars = typeof metadata?.maxTurnChars === "number" && Number.isSafeInteger(metadata.maxTurnChars) ? metadata.maxTurnChars : DEFAULT_MAX_IMPORT_TURN_CHARS;
4666
+ chunks.push({
4667
+ rawBytes: Buffer.byteLength(turn.rawContent, "utf8"),
4668
+ turnFingerprint: turn.turnFingerprint,
4669
+ timestamp: turn.timestamp ?? "",
4670
+ chunkIndex,
4671
+ chunkCount,
4672
+ maxTurnChars
4673
+ });
4674
+ reconstructed = nextReconstructed;
4675
+ if (reconstructed === importedContent) break;
4676
+ }
4677
+ if (reconstructed !== importedContent) return void 0;
4678
+ if (chunks.length === 0 || chunks.some(
4679
+ (chunk, index) => chunk.chunkIndex !== index || chunk.chunkCount !== chunks.length || chunk.maxTurnChars !== chunks[0].maxTurnChars || chunk.timestamp.length === 0
4680
+ )) {
4681
+ return void 0;
4682
+ }
4683
+ return chunks;
4684
+ }
4685
+ async function deferProcessedFlushPlanCleanupForMetadataFailure(params) {
4686
+ await writeProcessedMarker({
4687
+ markerPath: params.markerPath,
4688
+ marker: buildProcessedMarker({
4689
+ status: "processed",
4690
+ content: params.content,
4691
+ processedAt: params.processedAt,
4692
+ reason: params.reason,
4693
+ importTurns: params.importTurns
4694
+ })
4695
+ });
4696
+ return {
4697
+ status: "processed_cleanup_deferred",
4698
+ path: params.flushPlanPath,
4699
+ bytesProcessed: Buffer.byteLength(params.content, "utf8"),
4700
+ reason: FLUSH_PLAN_METADATA_FAILURE_CLEANUP_DEFERRED_REASON
4701
+ };
4702
+ }
4703
+ function buildFlushPlanImportTurns(params) {
4704
+ const maxTurnChars = normalizeMaxTurnChars(params.maxTurnChars);
4705
+ const chunks = splitFlushPlanContentIntoChunks({
4706
+ content: params.content,
4707
+ maxTurnChars
4708
+ });
4709
+ return chunks.map(
4710
+ (chunk, chunkIndex) => buildFlushPlanImportTurn({
4711
+ content: chunk,
4712
+ flushPlanPath: params.flushPlanPath,
4713
+ fingerprintPath: params.fingerprintPath,
4714
+ serviceId: params.serviceId,
4715
+ importedAt: params.importedAt,
4716
+ turnTimestamp: timestampForFlushPlanChunk(params.importedAt, chunkIndex),
4717
+ reason: params.reason,
4718
+ chunkIndex,
4719
+ chunkCount: chunks.length,
4720
+ maxTurnChars
4721
+ })
4722
+ );
4723
+ }
4724
+ async function readFlushPlanSnapshot(flushPlanPath) {
4725
+ let stat2;
4726
+ try {
4727
+ stat2 = await lstat2(flushPlanPath);
4728
+ } catch (error) {
4729
+ if (error?.code === "ENOENT") {
4730
+ return { status: "missing", path: flushPlanPath };
4731
+ }
4732
+ throw error;
4733
+ }
4734
+ if (stat2.isSymbolicLink()) {
4735
+ return {
4736
+ status: "skipped",
4737
+ path: flushPlanPath,
4738
+ reason: "flush plan path is a symlink"
4739
+ };
4740
+ }
4741
+ if (!stat2.isFile()) {
4742
+ return {
4743
+ status: "skipped",
4744
+ path: flushPlanPath,
4745
+ reason: "flush plan path is not a regular file"
4746
+ };
4747
+ }
4748
+ const content = await fileReader(flushPlanPath, "utf8");
4749
+ if (!isMeaningfulFlushPlanContent(content)) {
4750
+ return { status: "empty", path: flushPlanPath };
4751
+ }
4752
+ return { status: "ok", content };
4753
+ }
4754
+ function findProcessedMarkerPrefix(params) {
4755
+ const processedPrefix = utf8PrefixByByteLength(
4756
+ params.content,
4757
+ params.marker.processedBytes
4758
+ );
4759
+ if (processedPrefix !== void 0 && hashContent(processedPrefix) === params.marker.processedHash) {
4760
+ return processedPrefix;
4761
+ }
4762
+ if (params.marker.processedContent && params.content.startsWith(params.marker.processedContent) && hashContent(params.marker.processedContent) === params.marker.processedHash) {
4763
+ return params.marker.processedContent;
4764
+ }
4765
+ return void 0;
4766
+ }
4767
+ function splitContentByMarkerChunks(params) {
4768
+ const buffer = Buffer.from(params.content, "utf8");
4769
+ let offset = 0;
4770
+ const contentChunks = [];
4771
+ for (const chunk of params.chunks) {
4772
+ const nextOffset = offset + chunk.rawBytes;
4773
+ if (nextOffset > buffer.length) return void 0;
4774
+ const contentChunk = buffer.subarray(offset, nextOffset).toString("utf8");
4775
+ if (Buffer.byteLength(contentChunk, "utf8") !== chunk.rawBytes) {
4776
+ return void 0;
4777
+ }
4778
+ contentChunks.push(contentChunk);
4779
+ offset = nextOffset;
4780
+ }
4781
+ if (offset !== buffer.length) return void 0;
4782
+ return contentChunks;
4783
+ }
4784
+ function buildFlushPlanImportTurnsFromMarker(params) {
4785
+ if (!params.marker.processedChunks) return void 0;
4786
+ const contentChunks = splitContentByMarkerChunks({
4787
+ content: params.content.trim(),
4788
+ chunks: params.marker.processedChunks
4789
+ });
4790
+ if (!contentChunks) return void 0;
4791
+ return params.marker.processedChunks.map(
4792
+ (chunk, index) => buildFlushPlanImportTurn({
4793
+ content: contentChunks[index],
4794
+ flushPlanPath: params.flushPlanPath,
4795
+ fingerprintPath: params.fingerprintPath,
4796
+ serviceId: params.serviceId,
4797
+ importedAt: params.marker.processedAt,
4798
+ turnTimestamp: chunk.timestamp,
4799
+ turnFingerprint: chunk.turnFingerprint,
4800
+ reason: params.marker.reason,
4801
+ chunkIndex: chunk.chunkIndex,
4802
+ chunkCount: chunk.chunkCount,
4803
+ maxTurnChars: chunk.maxTurnChars
4804
+ })
4805
+ );
4806
+ }
4807
+ async function removeProcessedFlushPlanContent(params) {
4808
+ const processedBytes = Buffer.byteLength(params.processedContent, "utf8");
4809
+ let cleanupPath;
4810
+ try {
4811
+ const stat2 = await lstat2(params.flushPlanPath);
4812
+ if (stat2.isSymbolicLink()) {
4813
+ return {
4814
+ status: "processed_cleanup_deferred",
4815
+ path: params.flushPlanPath,
4816
+ bytesProcessed: processedBytes,
4817
+ reason: "flush plan path became a symlink before cleanup"
4818
+ };
4819
+ }
4820
+ if (!stat2.isFile()) {
4821
+ return {
4822
+ status: "processed_cleanup_deferred",
4823
+ path: params.flushPlanPath,
4824
+ bytesProcessed: processedBytes,
4825
+ reason: "flush plan path stopped being a regular file before cleanup"
4826
+ };
4827
+ }
4828
+ cleanupPath = resolveCleanupSnapshotPath(params.flushPlanPath);
4829
+ await rename(params.flushPlanPath, cleanupPath);
4830
+ } catch (error) {
4831
+ if (error?.code === "ENOENT") {
4832
+ return {
4833
+ status: "processed",
4834
+ path: params.flushPlanPath,
4835
+ bytesProcessed: processedBytes
4836
+ };
4837
+ }
4838
+ throw error;
4839
+ }
4840
+ try {
4841
+ await writeFile(params.flushPlanPath, "", { encoding: "utf8", flag: "wx" });
4842
+ } catch (error) {
4843
+ if (error?.code !== "EEXIST") {
4844
+ throw error;
4845
+ }
4846
+ }
4847
+ const current = await fileReader(cleanupPath, "utf8");
4848
+ const processedStatus = params.recoveredFromMarker ? "processed_marker_recovered" : "processed";
4849
+ const preservedTailStatus = params.recoveredFromMarker ? "processed_marker_recovered_tail" : "processed_preserved_tail";
4850
+ const matchIndex = current === params.processedContent || current.startsWith(params.processedContent) ? 0 : params.allowNonPrefixMatch ? current.indexOf(params.processedContent) : -1;
4851
+ if (matchIndex >= 0) {
4852
+ const preservedContent = current.slice(0, matchIndex) + current.slice(matchIndex + params.processedContent.length);
4853
+ if (preservedContent.length > 0) {
4854
+ const reconciled2 = await reconcileExistingFlushPlanReplacement({
4855
+ flushPlanPath: params.flushPlanPath,
4856
+ preservedContent
4857
+ });
4858
+ if (!reconciled2) {
4859
+ return {
4860
+ status: "processed_cleanup_deferred",
4861
+ path: params.flushPlanPath,
4862
+ bytesProcessed: processedBytes,
4863
+ reason: "flush plan path was recreated as an unsafe target during cleanup"
4864
+ };
4865
+ }
4866
+ }
4867
+ await unlink(cleanupPath);
4868
+ return {
4869
+ status: preservedContent.length > 0 ? preservedTailStatus : processedStatus,
4870
+ path: params.flushPlanPath,
4871
+ bytesProcessed: processedBytes,
4872
+ preservedBytes: preservedContent.length > 0 ? Buffer.byteLength(preservedContent, "utf8") : void 0
4873
+ };
4874
+ }
4875
+ const reconciled = await reconcileExistingFlushPlanReplacement({
4876
+ flushPlanPath: params.flushPlanPath,
4877
+ preservedContent: current
4878
+ });
4879
+ if (!reconciled) {
4880
+ return {
4881
+ status: "processed_cleanup_deferred",
4882
+ path: params.flushPlanPath,
4883
+ bytesProcessed: processedBytes,
4884
+ reason: "flush plan path was recreated as an unsafe target during cleanup"
4885
+ };
4886
+ }
4887
+ await unlink(cleanupPath);
4888
+ return {
4889
+ status: "processed_cleanup_deferred",
4890
+ path: params.flushPlanPath,
4891
+ bytesProcessed: processedBytes,
4892
+ reason: FLUSH_PLAN_CHANGED_BEFORE_CLEANUP_REASON
4893
+ };
4894
+ }
4895
+ async function removeProcessedFlushPlanPrefix(params) {
4896
+ return removeProcessedFlushPlanContent({
4897
+ flushPlanPath: params.flushPlanPath,
4898
+ processedContent: params.processedPrefix,
4899
+ recoveredFromMarker: params.recoveredFromMarker,
4900
+ allowNonPrefixMatch: false
4901
+ });
4902
+ }
4903
+ async function recoverProcessedFlushPlanPrefix(params) {
4904
+ const processedPrefix = findProcessedMarkerPrefix({
4905
+ content: params.content,
4906
+ marker: params.marker
4907
+ });
4908
+ if (processedPrefix) {
4909
+ const result = await removeProcessedFlushPlanPrefix({
4910
+ flushPlanPath: params.flushPlanPath,
4911
+ processedPrefix,
4912
+ recoveredFromMarker: true
4913
+ });
4914
+ if (result.status !== "processed_cleanup_deferred") {
4915
+ await removeProcessedMarker(params.markerPath);
4916
+ }
4917
+ return result;
4918
+ }
4919
+ await removeProcessedMarker(params.markerPath);
4920
+ return void 0;
4921
+ }
4922
+ async function recoverPendingFlushPlanImport(params) {
4923
+ const processedPrefix = findProcessedMarkerPrefix({
4924
+ content: params.content,
4925
+ marker: params.marker
4926
+ });
4927
+ if (!processedPrefix) {
4928
+ await removeProcessedMarker(params.markerPath);
4929
+ return void 0;
4930
+ }
4931
+ const importTurns = buildFlushPlanImportTurnsFromMarker({
4932
+ content: processedPrefix,
4933
+ marker: params.marker,
4934
+ flushPlanPath: params.flushPlanPath,
4935
+ fingerprintPath: params.fingerprintPath,
4936
+ serviceId: params.serviceId
4937
+ }) ?? buildFlushPlanImportTurns({
4938
+ content: processedPrefix,
4939
+ flushPlanPath: params.flushPlanPath,
4940
+ fingerprintPath: params.fingerprintPath,
4941
+ serviceId: params.serviceId,
4942
+ importedAt: params.marker.processedAt,
4943
+ reason: params.marker.reason,
4944
+ maxTurnChars: params.maxTurnChars
4945
+ });
4946
+ const ingestResult = await ingestFlushPlanImportTurns({
4947
+ ingestor: params.ingestor,
4948
+ importTurns,
4949
+ deadlineMs: params.deadlineMs
4950
+ });
4951
+ const handledResult = await handleFlushPlanIngestResult({
4952
+ ingestResult,
4953
+ importTurns,
4954
+ content: processedPrefix,
4955
+ flushPlanPath: params.flushPlanPath,
4956
+ markerPath: params.markerPath,
4957
+ processedAt: params.marker.processedAt,
4958
+ reason: params.marker.reason,
4959
+ logger: params.logger
4960
+ });
4961
+ if (handledResult) return handledResult;
4962
+ await writeProcessedMarker({
4963
+ markerPath: params.markerPath,
4964
+ marker: buildProcessedMarker({
4965
+ status: "processed",
4966
+ content: processedPrefix,
4967
+ processedAt: params.marker.processedAt,
4968
+ reason: params.marker.reason,
4969
+ importTurns
4970
+ })
4971
+ });
4972
+ const result = await removeProcessedFlushPlanPrefix({
4973
+ flushPlanPath: params.flushPlanPath,
4974
+ processedPrefix,
4975
+ recoveredFromMarker: true
4976
+ });
4977
+ if (result.status === "processed_cleanup_deferred") {
4978
+ params.logger?.warn(
4979
+ `OpenClaw flush-plan processed but cleanup deferred: ${result.reason}`
4980
+ );
4981
+ } else {
4982
+ await removeProcessedMarker(params.markerPath);
4983
+ }
4984
+ return result;
4985
+ }
4986
+ async function resolveFlushPlanFingerprintPath(params) {
4987
+ const workspaceRoot = path2.resolve(params.workspaceDir);
4988
+ const flushPlanPath = path2.resolve(params.flushPlanPath);
4989
+ const relativeFlushPlanPath = path2.relative(workspaceRoot, flushPlanPath);
4990
+ if (relativeFlushPlanPath.length > 0 && !relativeFlushPlanPath.startsWith("..") && !path2.isAbsolute(relativeFlushPlanPath)) {
4991
+ try {
4992
+ return path2.resolve(await realpath2(workspaceRoot), relativeFlushPlanPath);
4993
+ } catch (error) {
4994
+ if (error?.code !== "ENOENT") throw error;
4995
+ }
4996
+ }
4997
+ try {
4998
+ return await realpath2(flushPlanPath);
4999
+ } catch (error) {
5000
+ if (error?.code !== "ENOENT") throw error;
5001
+ return flushPlanPath;
5002
+ }
5003
+ }
5004
+ async function processOpenClawFlushPlanFile(params) {
5005
+ if (!params.enabled) return { status: "disabled" };
5006
+ const flushPlanPath = resolveOpenClawFlushPlanPath({
5007
+ workspaceDir: params.workspaceDir,
5008
+ serviceId: params.serviceId
5009
+ });
5010
+ const unsafeLocation = await validateFlushPlanLocation({
5011
+ workspaceDir: params.workspaceDir,
5012
+ flushPlanPath
5013
+ });
5014
+ if (unsafeLocation) return unsafeLocation;
5015
+ const markerPath = resolveProcessedMarkerPath(flushPlanPath);
5016
+ const processedMarker = await readProcessedMarker(markerPath);
5017
+ const cleanupRecovery = await recoverInterruptedCleanupSnapshots({
5018
+ flushPlanPath,
5019
+ logger: params.logger,
5020
+ marker: processedMarker,
5021
+ markerPath
5022
+ });
5023
+ const activeProcessedMarker = cleanupRecovery.removedProcessedMarker ? void 0 : processedMarker;
5024
+ const snapshot = await readFlushPlanSnapshot(flushPlanPath);
5025
+ if (snapshot.status !== "ok") {
5026
+ if (activeProcessedMarker && (snapshot.status === "empty" || snapshot.status === "missing")) {
5027
+ await removeProcessedMarker(markerPath);
5028
+ }
5029
+ return snapshot;
5030
+ }
5031
+ const fingerprintPath = await resolveFlushPlanFingerprintPath({
5032
+ workspaceDir: params.workspaceDir,
5033
+ flushPlanPath
5034
+ });
5035
+ if (activeProcessedMarker) {
5036
+ const recovered = activeProcessedMarker.status === "pending" ? await recoverPendingFlushPlanImport({
5037
+ flushPlanPath,
5038
+ fingerprintPath,
5039
+ markerPath,
5040
+ content: snapshot.content,
5041
+ marker: activeProcessedMarker,
5042
+ serviceId: params.serviceId,
5043
+ ingestor: params.ingestor,
5044
+ deadlineMs: params.deadlineMs,
5045
+ maxTurnChars: params.maxTurnChars,
5046
+ logger: params.logger
5047
+ }) : await recoverProcessedFlushPlanPrefix({
5048
+ flushPlanPath,
5049
+ markerPath,
5050
+ content: snapshot.content,
5051
+ marker: activeProcessedMarker
5052
+ });
5053
+ if (recovered) return recovered;
5054
+ }
5055
+ const importedAt = (params.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
5056
+ const reason = params.reason ?? "openclaw-flush-plan";
5057
+ const importTurns = buildFlushPlanImportTurns({
5058
+ content: snapshot.content,
5059
+ flushPlanPath,
5060
+ fingerprintPath,
5061
+ serviceId: params.serviceId,
5062
+ importedAt,
5063
+ reason,
5064
+ maxTurnChars: params.maxTurnChars
5065
+ });
5066
+ await mkdir(path2.dirname(flushPlanPath), { recursive: true });
5067
+ await writeProcessedMarker({
5068
+ markerPath,
5069
+ marker: buildProcessedMarker({
5070
+ status: "pending",
5071
+ content: snapshot.content,
5072
+ processedAt: importedAt,
5073
+ reason,
5074
+ importTurns
5075
+ })
5076
+ });
5077
+ const ingestResult = await ingestFlushPlanImportTurns({
5078
+ ingestor: params.ingestor,
5079
+ importTurns,
5080
+ deadlineMs: params.deadlineMs
5081
+ });
5082
+ const handledResult = await handleFlushPlanIngestResult({
5083
+ ingestResult,
5084
+ importTurns,
5085
+ content: snapshot.content,
5086
+ flushPlanPath,
5087
+ markerPath,
5088
+ processedAt: importedAt,
5089
+ reason,
5090
+ logger: params.logger
5091
+ });
5092
+ if (handledResult) return handledResult;
5093
+ await writeProcessedMarker({
5094
+ markerPath,
5095
+ marker: buildProcessedMarker({
5096
+ status: "processed",
5097
+ content: snapshot.content,
5098
+ processedAt: importedAt,
5099
+ reason,
5100
+ importTurns
5101
+ })
5102
+ });
5103
+ const result = await removeProcessedFlushPlanPrefix({
5104
+ flushPlanPath,
5105
+ processedPrefix: snapshot.content
5106
+ });
5107
+ if (result.status === "processed_cleanup_deferred") {
5108
+ params.logger?.warn(
5109
+ `OpenClaw flush-plan processed but cleanup deferred: ${result.reason}`
5110
+ );
5111
+ } else {
5112
+ await removeProcessedMarker(markerPath);
5113
+ }
5114
+ return result;
5115
+ }
5116
+
4009
5117
  // src/plugin-id.ts
4010
5118
  import { resolvePluginEntry } from "@remnic/core/plugin-entry-resolver";
4011
5119
  var REMNIC_OPENCLAW_PLUGIN_ID = "openclaw-remnic";
@@ -4139,23 +5247,23 @@ __export(resolve_provider_secret_exports, {
4139
5247
  findGatewayRuntimeModules: () => findGatewayRuntimeModules
4140
5248
  });
4141
5249
  __reExport(resolve_provider_secret_exports, resolve_provider_secret_star);
4142
- import path2 from "path";
5250
+ import path3 from "path";
4143
5251
  import * as resolve_provider_secret_star from "@remnic/core/resolve-provider-secret";
4144
5252
  async function findGatewayRuntimeModules(filePrefix) {
4145
5253
  const { existsSync, ["re"+"ad"+"Fi"+"le"+"Sync"]: fileReaderSync, readdirSync, realpathSync } = await import("fs");
4146
5254
  const { createRequire: createRequire2 } = await import("module");
4147
5255
  const candidates = [];
4148
5256
  const isWithinRoot = (root, candidate) => {
4149
- const relative = path2.relative(root, candidate);
4150
- return relative.length === 0 || !relative.startsWith("..") && !path2.isAbsolute(relative);
5257
+ const relative = path3.relative(root, candidate);
5258
+ return relative.length === 0 || !relative.startsWith("..") && !path3.isAbsolute(relative);
4151
5259
  };
4152
5260
  let packageRoot;
4153
5261
  try {
4154
5262
  const req = createRequire2(import.meta.url);
4155
5263
  const openclawEntrypoint = realpathSync(req.resolve("openclaw"));
4156
- let currentDir = path2.dirname(openclawEntrypoint);
5264
+ let currentDir = path3.dirname(openclawEntrypoint);
4157
5265
  while (true) {
4158
- const packageJsonPath = path2.join(currentDir, "package.json");
5266
+ const packageJsonPath = path3.join(currentDir, "package.json");
4159
5267
  if (existsSync(packageJsonPath)) {
4160
5268
  const packageJson = JSON.parse(fileReaderSync(packageJsonPath, "utf8"));
4161
5269
  if (packageJson.name !== "openclaw") {
@@ -4164,7 +5272,7 @@ async function findGatewayRuntimeModules(filePrefix) {
4164
5272
  packageRoot = realpathSync(currentDir);
4165
5273
  break;
4166
5274
  }
4167
- const parent = path2.dirname(currentDir);
5275
+ const parent = path3.dirname(currentDir);
4168
5276
  if (parent === currentDir) {
4169
5277
  return [];
4170
5278
  }
@@ -4174,14 +5282,14 @@ async function findGatewayRuntimeModules(filePrefix) {
4174
5282
  return [];
4175
5283
  }
4176
5284
  try {
4177
- const distDir = realpathSync(path2.join(packageRoot, "dist"));
5285
+ const distDir = realpathSync(path3.join(packageRoot, "dist"));
4178
5286
  if (!isWithinRoot(packageRoot, distDir)) {
4179
5287
  return [];
4180
5288
  }
4181
5289
  const files = readdirSync(distDir);
4182
5290
  for (const f of files) {
4183
5291
  if (f.startsWith(filePrefix) && f.endsWith(".js")) {
4184
- const candidate = realpathSync(path2.join(distDir, f));
5292
+ const candidate = realpathSync(path3.join(distDir, f));
4185
5293
  if (isWithinRoot(packageRoot, candidate) && isWithinRoot(distDir, candidate)) {
4186
5294
  candidates.push(candidate);
4187
5295
  }
@@ -4515,6 +5623,7 @@ function buildServiceKeys(serviceId) {
4515
5623
  ACCESS_SERVICE: `__openclawEngramAccessService${suffix}`,
4516
5624
  ACCESS_HTTP_SERVER: `__openclawEngramAccessHttpServer${suffix}`,
4517
5625
  ACCESS_HTTP_AUTH_STATE: `__openclawEngramAccessHttpAuthState${suffix}`,
5626
+ FLUSH_PLAN_PROCESSING_CHAINS: `__openclawEngramFlushPlanProcessingChains${suffix}`,
4518
5627
  SERVICE_STARTED: `__openclawEngramServiceStarted${suffix}`,
4519
5628
  INIT_PROMISE: `__openclawEngramInitPromise${suffix}`,
4520
5629
  ORCHESTRATOR: `__openclawEngramOrchestrator${suffix}`
@@ -4525,7 +5634,7 @@ function resolveOpenClawConfigFilePath() {
4525
5634
  if (explicitConfigPath && explicitConfigPath.length > 0) {
4526
5635
  return expandTildePath(explicitConfigPath);
4527
5636
  }
4528
- return path3.join(resolveHomeDir(), ".openclaw", "openclaw.json");
5637
+ return path4.join(resolveHomeDir(), ".openclaw", "openclaw.json");
4529
5638
  }
4530
5639
  function coerceRawConfigBoolean(value) {
4531
5640
  if (typeof value === "boolean") return value;
@@ -4568,7 +5677,7 @@ function readPluginHooksPolicy(apiConfig, pluginId) {
4568
5677
  }
4569
5678
  async function maybeRegisterLiveConnectorCron(orchestrator) {
4570
5679
  if (!hasEnabledLiveConnectorConfig(orchestrator.config.connectors)) return;
4571
- const jobsPath = path3.join(resolveHomeDir(), ".openclaw", "cron", "jobs.json");
5680
+ const jobsPath = path4.join(resolveHomeDir(), ".openclaw", "cron", "jobs.json");
4572
5681
  try {
4573
5682
  if (!fileExistsNow(jobsPath)) {
4574
5683
  logger_exports.log.debug("live connectors cron: jobs.json not found, skipping auto-register");
@@ -4829,7 +5938,7 @@ function stableOpenClawConfigSignature(value, seen = /* @__PURE__ */ new WeakSet
4829
5938
  return JSON.stringify(String(value));
4830
5939
  }
4831
5940
  function openClawHostEmbeddingConfigSignature(cfg, apiConfig, workspaceDir) {
4832
- return `sha256:${createHash2("sha256").update(JSON.stringify({
5941
+ return `sha256:${createHash3("sha256").update(JSON.stringify({
4833
5942
  enabled: cfg.hostEmbeddingProviderEnabled !== false,
4834
5943
  memoryDir: cfg.memoryDir,
4835
5944
  providerId: cfg.hostEmbeddingProviderId ?? "",
@@ -4956,6 +6065,7 @@ var pluginDefinition = {
4956
6065
  void migrationPromise;
4957
6066
  }
4958
6067
  const fileConfig = loadPluginConfigFromFile(serviceId);
6068
+ const openclawFlushPlanProcessingEnabled = resolveOpenClawFlushPlanProcessingEnabledFromConfig(fileConfig, api.pluginConfig);
4959
6069
  const cfg = (0, config_exports.parseConfig)({
4960
6070
  ...fileConfig,
4961
6071
  // File-backed fallback for runtimes that omit pluginConfig
@@ -5070,9 +6180,9 @@ var pluginDefinition = {
5070
6180
  emitLegacyTools: cfg.emitLegacyTools
5071
6181
  });
5072
6182
  globalThis[keys.ACCESS_HTTP_SERVER] = accessHttpServer;
5073
- const pluginStateDir = path3.join(cfg.memoryDir, "state", "plugins", serviceId);
5074
- const togglePrimaryPath = path3.join(pluginStateDir, "session-toggles.json");
5075
- const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path3.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
6183
+ const pluginStateDir = path4.join(cfg.memoryDir, "state", "plugins", serviceId);
6184
+ const togglePrimaryPath = path4.join(pluginStateDir, "session-toggles.json");
6185
+ const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path4.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
5076
6186
  const sessionToggleStore = createFileToggleStore(togglePrimaryPath, {
5077
6187
  secondaryReadOnlyPath: toggleSecondaryPath
5078
6188
  });
@@ -5094,11 +6204,103 @@ var pluginDefinition = {
5094
6204
  }
5095
6205
  function resolveDreamJournalPath(runtimeWorkspaceDir) {
5096
6206
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
5097
- return path3.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path3.join(workspaceRoot, cfg.dreaming.journalPath);
6207
+ return path4.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path4.join(workspaceRoot, cfg.dreaming.journalPath);
5098
6208
  }
5099
6209
  function resolveHeartbeatJournalPath(runtimeWorkspaceDir) {
5100
6210
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
5101
- return path3.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path3.join(workspaceRoot, cfg.heartbeat.journalPath);
6211
+ return path4.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path4.join(workspaceRoot, cfg.heartbeat.journalPath);
6212
+ }
6213
+ const existingFlushPlanProcessingChains = globalThis[keys.FLUSH_PLAN_PROCESSING_CHAINS];
6214
+ const flushPlanProcessingChains = existingFlushPlanProcessingChains instanceof Map ? existingFlushPlanProcessingChains : /* @__PURE__ */ new Map();
6215
+ globalThis[keys.FLUSH_PLAN_PROCESSING_CHAINS] = flushPlanProcessingChains;
6216
+ function resolveFlushPlanDeadlineMs(options) {
6217
+ if (typeof options.deadlineMs === "number" && Number.isFinite(options.deadlineMs)) {
6218
+ return options.deadlineMs;
6219
+ }
6220
+ if (typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0) {
6221
+ return Date.now() + Math.floor(options.timeoutMs);
6222
+ }
6223
+ return void 0;
6224
+ }
6225
+ function waitForOpenClawFlushPlanTask(task, reason, deadlineMs) {
6226
+ if (typeof deadlineMs !== "number") {
6227
+ return task;
6228
+ }
6229
+ const remainingMs = Math.max(0, deadlineMs - Date.now());
6230
+ if (remainingMs === 0) {
6231
+ logger_exports.log.warn(
6232
+ `OpenClaw flush-plan processing timed out before queue wait for ${reason}`
6233
+ );
6234
+ return Promise.resolve();
6235
+ }
6236
+ let timeout;
6237
+ return Promise.race([
6238
+ task,
6239
+ new Promise((resolve) => {
6240
+ timeout = setTimeout(() => {
6241
+ logger_exports.log.warn(
6242
+ `OpenClaw flush-plan processing timed out while waiting for ${reason}; current drain remains fenced until it settles`
6243
+ );
6244
+ resolve();
6245
+ }, remainingMs);
6246
+ })
6247
+ ]).finally(() => {
6248
+ if (timeout) clearTimeout(timeout);
6249
+ });
6250
+ }
6251
+ async function runOpenClawFlushPlanProcessing(reason, workspaceRoot, deadlineMs) {
6252
+ if (typeof deadlineMs === "number" && Date.now() >= deadlineMs) {
6253
+ logger_exports.log.warn(
6254
+ `OpenClaw flush-plan processing timed out before starting for ${reason}`
6255
+ );
6256
+ return;
6257
+ }
6258
+ try {
6259
+ const result = await processOpenClawFlushPlanFile({
6260
+ enabled: openclawFlushPlanProcessingEnabled,
6261
+ workspaceDir: workspaceRoot,
6262
+ serviceId,
6263
+ ingestor: orchestrator,
6264
+ logger: {
6265
+ debug: (message) => logger_exports.log.debug(message),
6266
+ info: (message) => logger_exports.log.info(message),
6267
+ warn: (message) => logger_exports.log.warn(message)
6268
+ },
6269
+ reason,
6270
+ deadlineMs,
6271
+ maxTurnChars: cfg.extractionMaxTurnChars
6272
+ });
6273
+ if (result.status === "processed" || result.status === "processed_preserved_tail" || result.status === "processed_marker_recovered" || result.status === "processed_marker_recovered_tail" || result.status === "processed_cleanup_deferred") {
6274
+ logger_exports.log.info(
6275
+ `OpenClaw flush-plan ${result.status}: ${result.bytesProcessed ?? 0} bytes` + (result.preservedBytes ? ` (${result.preservedBytes} bytes preserved)` : "")
6276
+ );
6277
+ } else if (result.status === "skipped") {
6278
+ logger_exports.log.warn(
6279
+ `OpenClaw flush-plan processing skipped: ${result.reason ?? "unknown reason"}`
6280
+ );
6281
+ }
6282
+ } catch (error) {
6283
+ const detail = displayErrorDetail(error) || "unknown error";
6284
+ logger_exports.log.warn(`OpenClaw flush-plan processing failed: ${detail}`);
6285
+ }
6286
+ }
6287
+ async function queueOpenClawFlushPlanProcessing(reason, runtimeWorkspaceDir, options = {}) {
6288
+ if (passiveMode || !openclawFlushPlanProcessingEnabled) {
6289
+ return Promise.resolve();
6290
+ }
6291
+ const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
6292
+ const chainKey = await resolveFlushPlanProcessingChainKey(workspaceRoot);
6293
+ const deadlineMs = resolveFlushPlanDeadlineMs(options);
6294
+ const previousTask = flushPlanProcessingChains.get(chainKey);
6295
+ const task = (previousTask ?? Promise.resolve()).catch(() => void 0).then(() => runOpenClawFlushPlanProcessing(reason, workspaceRoot, deadlineMs));
6296
+ const fencedTask = task.catch(() => void 0);
6297
+ flushPlanProcessingChains.set(chainKey, fencedTask);
6298
+ void fencedTask.finally(() => {
6299
+ if (flushPlanProcessingChains.get(chainKey) === fencedTask) {
6300
+ flushPlanProcessingChains.delete(chainKey);
6301
+ }
6302
+ });
6303
+ return waitForOpenClawFlushPlanTask(task, reason, deadlineMs);
5102
6304
  }
5103
6305
  function queueDreamSurfaceSync(runtimeWorkspaceDir) {
5104
6306
  if (!cfg.dreaming.enabled) return Promise.resolve();
@@ -5282,7 +6484,7 @@ Keep the reflection grounded in the evidence below.
5282
6484
  timeoutMs: cfg.activeRecallTimeoutMs,
5283
6485
  cacheTtlMs: cfg.activeRecallCacheTtlMs,
5284
6486
  persistTranscripts: cfg.activeRecallPersistTranscripts,
5285
- transcriptDir: path3.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path3.join(pluginStateDir, cfg.activeRecallTranscriptDir),
6487
+ transcriptDir: path4.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path4.join(pluginStateDir, cfg.activeRecallTranscriptDir),
5286
6488
  entityGraphDepth: cfg.activeRecallEntityGraphDepth,
5287
6489
  includeCausalTrajectories: cfg.activeRecallIncludeCausalTrajectories,
5288
6490
  includeDaySummary: cfg.activeRecallIncludeDaySummary,
@@ -5367,7 +6569,7 @@ Keep the reflection grounded in the evidence below.
5367
6569
  }
5368
6570
  }, buildInlineExplicitCaptureDedupeKey2 = function(messageKey, note) {
5369
6571
  if (!messageKey) return null;
5370
- return `${messageKey}:inline-memory-note:${createHash2("sha256").update(JSON.stringify(note)).digest("hex")}`;
6572
+ return `${messageKey}:inline-memory-note:${createHash3("sha256").update(JSON.stringify(note)).digest("hex")}`;
5371
6573
  }, resolveStoredCodexThreadId2 = function(sessionKey) {
5372
6574
  const threadId = codexThreadBySession.get(sessionKey);
5373
6575
  return typeof threadId === "string" && threadId.length > 0 ? threadId : null;
@@ -6289,20 +7491,20 @@ Keep the reflection grounded in the evidence below.
6289
7491
  const remnicQmdCommand = typeof orchestrator.config.qmdPath === "string" && orchestrator.config.qmdPath.trim().length > 0 ? orchestrator.config.qmdPath.trim() : "qmd";
6290
7492
  const readAllowedRoots = [
6291
7493
  orchestrator.config.memoryDir,
6292
- capabilityWorkspaceDir ? path3.join(capabilityWorkspaceDir, "memory") : void 0
7494
+ capabilityWorkspaceDir ? path4.join(capabilityWorkspaceDir, "memory") : void 0
6293
7495
  ].filter((root) => typeof root === "string" && root.length > 0);
6294
7496
  const canonicalizeRootForContainment = async (rawPath) => {
6295
- const resolved = path3.resolve(rawPath);
7497
+ const resolved = path4.resolve(rawPath);
6296
7498
  try {
6297
- return path3.normalize(await realPathLater(resolved));
7499
+ return path4.normalize(await realPathLater(resolved));
6298
7500
  } catch {
6299
- return path3.normalize(resolved);
7501
+ return path4.normalize(resolved);
6300
7502
  }
6301
7503
  };
6302
7504
  const canonicalizeForRead = async (rawPath) => {
6303
- const resolved = path3.resolve(rawPath);
7505
+ const resolved = path4.resolve(rawPath);
6304
7506
  const real = await realPathLater(resolved);
6305
- return path3.normalize(real);
7507
+ return path4.normalize(real);
6306
7508
  };
6307
7509
  const readAllowedCanonicalRootsPromise = Promise.all(
6308
7510
  readAllowedRoots.map((root) => canonicalizeRootForContainment(root))
@@ -6316,29 +7518,29 @@ Keep the reflection grounded in the evidence below.
6316
7518
  }
6317
7519
  const canonicalRoots = await readAllowedCanonicalRootsPromise;
6318
7520
  return canonicalRoots.some((root) => {
6319
- const relative = path3.relative(root, canonicalCandidatePath);
6320
- return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
7521
+ const relative = path4.relative(root, canonicalCandidatePath);
7522
+ return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
6321
7523
  });
6322
7524
  };
6323
7525
  const normalizeWorkspacePath = (rawPath) => {
6324
7526
  if (!rawPath || typeof rawPath !== "string") return "memory";
6325
- const resolved = path3.isAbsolute(rawPath) ? path3.resolve(rawPath) : path3.resolve(capabilityWorkspaceDir, rawPath);
6326
- const relative = path3.relative(capabilityWorkspaceDir, resolved);
6327
- return relative && !relative.startsWith("..") && !path3.isAbsolute(relative) ? relative : rawPath;
7527
+ const resolved = path4.isAbsolute(rawPath) ? path4.resolve(rawPath) : path4.resolve(capabilityWorkspaceDir, rawPath);
7528
+ const relative = path4.relative(capabilityWorkspaceDir, resolved);
7529
+ return relative && !relative.startsWith("..") && !path4.isAbsolute(relative) ? relative : rawPath;
6328
7530
  };
6329
7531
  const relativizeToMemoryRoot = (rawPath) => {
6330
7532
  if (!rawPath || typeof rawPath !== "string") return "memory";
6331
- const resolved = path3.isAbsolute(rawPath) ? path3.resolve(rawPath) : path3.resolve(capabilityWorkspaceDir, rawPath);
7533
+ const resolved = path4.isAbsolute(rawPath) ? path4.resolve(rawPath) : path4.resolve(capabilityWorkspaceDir, rawPath);
6332
7534
  for (const root of readAllowedRoots) {
6333
- const relative = path3.relative(root, resolved);
6334
- if (relative !== "" && !relative.startsWith("..") && !path3.isAbsolute(relative)) {
7535
+ const relative = path4.relative(root, resolved);
7536
+ if (relative !== "" && !relative.startsWith("..") && !path4.isAbsolute(relative)) {
6335
7537
  return relative;
6336
7538
  }
6337
7539
  }
6338
7540
  return normalizeWorkspacePath(rawPath);
6339
7541
  };
6340
7542
  const resolveReadablePath = async (requestedPath) => {
6341
- const candidateAbsolutePaths = path3.isAbsolute(requestedPath) ? [path3.resolve(requestedPath)] : readAllowedRoots.map((root) => path3.resolve(root, requestedPath));
7543
+ const candidateAbsolutePaths = path4.isAbsolute(requestedPath) ? [path4.resolve(requestedPath)] : readAllowedRoots.map((root) => path4.resolve(root, requestedPath));
6342
7544
  let canonicalPath;
6343
7545
  let lastError;
6344
7546
  for (const absolutePath of candidateAbsolutePaths) {
@@ -6356,8 +7558,8 @@ Keep the reflection grounded in the evidence below.
6356
7558
  }
6357
7559
  const canonicalRoots = await readAllowedCanonicalRootsPromise;
6358
7560
  const contained = canonicalRoots.some((root) => {
6359
- const relative = path3.relative(root, canonicalPath);
6360
- return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
7561
+ const relative = path4.relative(root, canonicalPath);
7562
+ return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
6361
7563
  });
6362
7564
  if (!contained) {
6363
7565
  throw new Error(`memory read outside allowed roots: ${requestedPath}`);
@@ -6389,15 +7591,15 @@ Keep the reflection grounded in the evidence below.
6389
7591
  }).map((result, index) => {
6390
7592
  const candidate = result;
6391
7593
  const rawPath = typeof candidate.path === "string" ? candidate.path : typeof candidate.id === "string" ? candidate.id : `memory-${index + 1}`;
6392
- const absolutePath = path3.isAbsolute(rawPath) ? path3.resolve(rawPath) : (() => {
7594
+ const absolutePath = path4.isAbsolute(rawPath) ? path4.resolve(rawPath) : (() => {
6393
7595
  for (const root of readAllowedRoots) {
6394
- const candidateAbs = path3.resolve(root, rawPath);
6395
- const relative = path3.relative(root, candidateAbs);
6396
- if (!relative.startsWith("..") && !path3.isAbsolute(relative)) {
7596
+ const candidateAbs = path4.resolve(root, rawPath);
7597
+ const relative = path4.relative(root, candidateAbs);
7598
+ if (!relative.startsWith("..") && !path4.isAbsolute(relative)) {
6397
7599
  return candidateAbs;
6398
7600
  }
6399
7601
  }
6400
- return path3.resolve(capabilityWorkspaceDir, rawPath);
7602
+ return path4.resolve(capabilityWorkspaceDir, rawPath);
6401
7603
  })();
6402
7604
  const normalizedPath = relativizeToMemoryRoot(rawPath);
6403
7605
  const startLine = typeof candidate.startLine === "number" && Number.isFinite(candidate.startLine) ? Math.max(1, Math.floor(candidate.startLine)) : 1;
@@ -6889,6 +8091,7 @@ Keep the reflection grounded in the evidence below.
6889
8091
  async (event, ctx) => {
6890
8092
  const sessionKey = ctx?.sessionKey ?? event?.sessionKey ?? "default";
6891
8093
  const sessionIdentity = resolveSessionIdentity2(sessionKey, event, ctx);
8094
+ const workspaceDir = ctx?.workspaceDir || event?.workspaceDir || orchestrator.config.workspaceDir || defaultWorkspaceDir();
6892
8095
  try {
6893
8096
  clearCodexCompatCaches2(sessionKey, sessionIdentity.providerThreadId, {
6894
8097
  preserveMessageCount: true,
@@ -6922,6 +8125,9 @@ Keep the reflection grounded in the evidence below.
6922
8125
  logger_exports.log.debug(`LCM after_compaction error: ${lcmErr}`);
6923
8126
  }
6924
8127
  }
8128
+ await queueOpenClawFlushPlanProcessing("after_compaction", workspaceDir, {
8129
+ timeoutMs: cfg.beforeResetTimeoutMs
8130
+ });
6925
8131
  if (!orchestrator.config.compactionResetEnabled) {
6926
8132
  logger_exports.log.debug(
6927
8133
  `compaction completed for ${sessionKey}, reset disabled \u2014 skipping`
@@ -6931,7 +8137,6 @@ Keep the reflection grounded in the evidence below.
6931
8137
  logger_exports.log.info(
6932
8138
  `compaction completed for ${sessionKey}, triggering session reset`
6933
8139
  );
6934
- const workspaceDir = ctx?.workspaceDir || event?.workspaceDir || orchestrator.config.workspaceDir || defaultWorkspaceDir();
6935
8140
  const apiAny = api;
6936
8141
  if (typeof apiAny.resetSession === "function") {
6937
8142
  const result = await apiAny.resetSession(sessionKey, "new");
@@ -6940,7 +8145,7 @@ Keep the reflection grounded in the evidence below.
6940
8145
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
6941
8146
  );
6942
8147
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
6943
- const signalPath = path3.join(
8148
+ const signalPath = path4.join(
6944
8149
  workspaceDir,
6945
8150
  `.compaction-reset-signal-${safeSessionKey}`
6946
8151
  );
@@ -6982,6 +8187,7 @@ Keep the reflection grounded in the evidence below.
6982
8187
  sessionIdentity
6983
8188
  );
6984
8189
  const flushEnabled = cfg.flushOnResetEnabled && typeof orchestrator.flushSession === "function";
8190
+ const beforeResetDeadlineMs = Date.now() + cfg.beforeResetTimeoutMs;
6985
8191
  const flushAbort = new AbortController();
6986
8192
  let flushTimedOut = false;
6987
8193
  let flushFailed = false;
@@ -7008,17 +8214,26 @@ Keep the reflection grounded in the evidence below.
7008
8214
  const boundedFlush = flushEnabled ? Promise.race([
7009
8215
  flushPromise,
7010
8216
  new Promise((resolve) => {
8217
+ const remainingMs = Math.max(
8218
+ 0,
8219
+ beforeResetDeadlineMs - Date.now()
8220
+ );
7011
8221
  timeoutId = setTimeout(() => {
7012
8222
  flushTimedOut = true;
7013
8223
  flushAbort.abort();
7014
8224
  resolve();
7015
- }, cfg.beforeResetTimeoutMs);
8225
+ }, remainingMs);
7016
8226
  })
7017
8227
  ]) : flushPromise;
7018
8228
  await boundedFlush;
7019
8229
  if (timeoutId) {
7020
8230
  clearTimeout(timeoutId);
7021
8231
  }
8232
+ await queueOpenClawFlushPlanProcessing(
8233
+ "before_reset",
8234
+ ctx?.workspaceDir || event?.workspaceDir,
8235
+ { deadlineMs: beforeResetDeadlineMs }
8236
+ );
7022
8237
  if (flushTimedOut) {
7023
8238
  logger_exports.log.warn(
7024
8239
  `before_reset flush timed out after ${cfg.beforeResetTimeoutMs}ms`
@@ -7040,7 +8255,7 @@ Keep the reflection grounded in the evidence below.
7040
8255
  if (!passiveMode && sdkCaps.hasBeforePromptBuild) {
7041
8256
  api.on(
7042
8257
  "session_start",
7043
- async (event, _ctx) => {
8258
+ async (event, ctx) => {
7044
8259
  const sessionKey = event.sessionKey ?? "default";
7045
8260
  logger_exports.log.debug(`session_start: ${sessionKey}`);
7046
8261
  try {
@@ -7048,6 +8263,11 @@ Keep the reflection grounded in the evidence below.
7048
8263
  } catch (err) {
7049
8264
  logger_exports.log.debug(`session_start file hygiene failed: ${err}`);
7050
8265
  }
8266
+ void queueOpenClawFlushPlanProcessing(
8267
+ "session_start",
8268
+ ctx?.workspaceDir ?? event.workspaceDir,
8269
+ { timeoutMs: cfg.beforeResetTimeoutMs }
8270
+ );
7051
8271
  }
7052
8272
  );
7053
8273
  api.on(
@@ -7086,6 +8306,11 @@ Keep the reflection grounded in the evidence below.
7086
8306
  if (orchestrator.config.compactionResetEnabled) {
7087
8307
  orchestrator.clearRecallWorkspaceOverride(sessionKey);
7088
8308
  }
8309
+ await queueOpenClawFlushPlanProcessing(
8310
+ "session_end",
8311
+ ctx?.workspaceDir || event?.workspaceDir,
8312
+ { timeoutMs: cfg.beforeResetTimeoutMs }
8313
+ );
7089
8314
  }
7090
8315
  );
7091
8316
  api.on(
@@ -7182,7 +8407,7 @@ Keep the reflection grounded in the evidence below.
7182
8407
  }
7183
8408
  async function ensureHourlySummaryCron(api2) {
7184
8409
  const jobId = "engram-hourly-summary";
7185
- const cronFilePath = path3.join(
8410
+ const cronFilePath = path4.join(
7186
8411
  os.homedir(),
7187
8412
  ".openclaw",
7188
8413
  "cron",
@@ -7241,32 +8466,32 @@ Keep the reflection grounded in the evidence below.
7241
8466
  };
7242
8467
  const normalizeCorpusPath = (value) => value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
7243
8468
  const pathIsInside = (root, candidate) => {
7244
- const relative = path3.relative(root, candidate);
7245
- return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
8469
+ const relative = path4.relative(root, candidate);
8470
+ return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
7246
8471
  };
7247
8472
  const corpusPathCandidates = (rawPath, storageDir) => {
7248
8473
  const candidates = /* @__PURE__ */ new Set();
7249
8474
  const trimmed = rawPath.trim();
7250
8475
  if (!trimmed) return [];
7251
8476
  candidates.add(normalizeCorpusPath(trimmed));
7252
- if (path3.isAbsolute(trimmed)) {
7253
- const absolutePath = path3.resolve(trimmed);
7254
- const absoluteStorageDir = path3.resolve(storageDir);
8477
+ if (path4.isAbsolute(trimmed)) {
8478
+ const absolutePath = path4.resolve(trimmed);
8479
+ const absoluteStorageDir = path4.resolve(storageDir);
7255
8480
  if (pathIsInside(absoluteStorageDir, absolutePath)) {
7256
- candidates.add(normalizeCorpusPath(path3.relative(absoluteStorageDir, absolutePath)));
8481
+ candidates.add(normalizeCorpusPath(path4.relative(absoluteStorageDir, absolutePath)));
7257
8482
  }
7258
8483
  }
7259
- candidates.add(path3.basename(trimmed));
8484
+ candidates.add(path4.basename(trimmed));
7260
8485
  return [...candidates].filter((candidate) => candidate.length > 0);
7261
8486
  };
7262
8487
  const displayCorpusPath = (rawPath, storageDir) => {
7263
8488
  const trimmed = rawPath.trim();
7264
8489
  if (!trimmed) return "";
7265
- if (path3.isAbsolute(trimmed)) {
7266
- const absolutePath = path3.resolve(trimmed);
7267
- const absoluteStorageDir = path3.resolve(storageDir);
8490
+ if (path4.isAbsolute(trimmed)) {
8491
+ const absolutePath = path4.resolve(trimmed);
8492
+ const absoluteStorageDir = path4.resolve(storageDir);
7268
8493
  if (pathIsInside(absoluteStorageDir, absolutePath)) {
7269
- return normalizeCorpusPath(path3.relative(absoluteStorageDir, absolutePath));
8494
+ return normalizeCorpusPath(path4.relative(absoluteStorageDir, absolutePath));
7270
8495
  }
7271
8496
  }
7272
8497
  return normalizeCorpusPath(trimmed);
@@ -7528,6 +8753,11 @@ Keep the reflection grounded in the evidence below.
7528
8753
  logger_exports.log.info(
7529
8754
  `gateway_start fired \u2014 Remnic memory plugin is active (id=${pluginDefinition.id}, memoryDir=${cfg.memoryDir})`
7530
8755
  );
8756
+ void queueOpenClawFlushPlanProcessing(
8757
+ "gateway_start",
8758
+ getOpenClawRuntimeWorkspaceDir(api),
8759
+ { timeoutMs: cfg.beforeResetTimeoutMs }
8760
+ );
7531
8761
  } catch (err) {
7532
8762
  try {
7533
8763
  activeOpikExporter?.unsubscribe();
@@ -7616,6 +8846,9 @@ Keep the reflection grounded in the evidence below.
7616
8846
  delete globalThis[keys.ACCESS_HTTP_SERVER];
7617
8847
  delete globalThis[keys.ACCESS_HTTP_AUTH_STATE];
7618
8848
  delete globalThis[keys.ACCESS_SERVICE];
8849
+ if (flushPlanProcessingChains.size === 0) {
8850
+ delete globalThis[keys.FLUSH_PLAN_PROCESSING_CHAINS];
8851
+ }
7619
8852
  }
7620
8853
  if (!secondaryTookOver) {
7621
8854
  globalThis[keys.HOOK_APIS] = /* @__PURE__ */ new WeakSet();
@@ -7629,6 +8862,44 @@ Keep the reflection grounded in the evidence below.
7629
8862
  }
7630
8863
  };
7631
8864
  var src_default = tryDefinePluginEntry(pluginDefinition);
8865
+ var OPENCLAW_BOOLEAN_ACCEPTED_VALUES = "true/false/1/0/yes/no/on/off";
8866
+ function coerceOpenClawBooleanLike(value) {
8867
+ if (typeof value === "boolean") return value;
8868
+ if (typeof value !== "string") return void 0;
8869
+ const normalized = value.trim().toLowerCase();
8870
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
8871
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
8872
+ return void 0;
8873
+ }
8874
+ function resolveOpenClawFlushPlanProcessingEnabled(configValue) {
8875
+ if (configValue === void 0 || configValue === null) return true;
8876
+ const coerced = coerceOpenClawBooleanLike(configValue);
8877
+ if (coerced === void 0) {
8878
+ throw new Error(
8879
+ `openclawFlushPlanProcessingEnabled must be a boolean-like value (${OPENCLAW_BOOLEAN_ACCEPTED_VALUES}); got ${JSON.stringify(configValue)}`
8880
+ );
8881
+ }
8882
+ return coerced;
8883
+ }
8884
+ function resolveOpenClawFlushPlanProcessingEnabledFromConfig(fileConfig, runtimeConfig) {
8885
+ const runtimeRecord = runtimeConfig && typeof runtimeConfig === "object" ? runtimeConfig : void 0;
8886
+ const hasRuntimeValue = runtimeRecord !== void 0 && Object.prototype.hasOwnProperty.call(
8887
+ runtimeRecord,
8888
+ "openclawFlushPlanProcessingEnabled"
8889
+ );
8890
+ const hasFileValue = fileConfig !== void 0 && Object.prototype.hasOwnProperty.call(
8891
+ fileConfig,
8892
+ "openclawFlushPlanProcessingEnabled"
8893
+ );
8894
+ const runtimeValue = hasRuntimeValue ? resolveOpenClawFlushPlanProcessingEnabled(
8895
+ runtimeRecord.openclawFlushPlanProcessingEnabled
8896
+ ) : void 0;
8897
+ const fileValue = hasFileValue ? resolveOpenClawFlushPlanProcessingEnabled(
8898
+ fileConfig.openclawFlushPlanProcessingEnabled
8899
+ ) : void 0;
8900
+ if (runtimeValue === false || fileValue === false) return false;
8901
+ return fileValue ?? runtimeValue ?? true;
8902
+ }
7632
8903
  function extractLastTurn(messages) {
7633
8904
  let lastUserIdx = -1;
7634
8905
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -7701,7 +8972,7 @@ function buildOpenClawInboundContentFingerprint(content, message, event, ctx, se
7701
8972
  normalizeThreadId(message.threadId) ?? normalizeThreadId(event.threadId) ?? normalizeThreadId(ctx.threadId) ?? "",
7702
8973
  512
7703
8974
  );
7704
- const contentHash = createHash2("sha256").update(normalizedContent).digest("hex");
8975
+ const contentHash = createHash3("sha256").update(normalizedContent).digest("hex");
7705
8976
  return [
7706
8977
  sessionPart,
7707
8978
  "content",
@@ -7715,7 +8986,7 @@ function buildOpenClawSparseInboundContentFingerprint(content, sessionKey) {
7715
8986
  const normalizedContent = content.trim();
7716
8987
  if (!normalizedContent) return null;
7717
8988
  const sessionPart = truncateMetadataValue(sessionKey || "default", 512);
7718
- const contentHash = createHash2("sha256").update(normalizedContent).digest("hex");
8989
+ const contentHash = createHash3("sha256").update(normalizedContent).digest("hex");
7719
8990
  return `${sessionPart}\0sparse-content\0${contentHash}`;
7720
8991
  }
7721
8992
  function withReplyExtractionHint(content, metadata) {
@@ -7739,10 +9010,18 @@ function normalizeThreadId(value) {
7739
9010
  function truncateMetadataValue(value, maxChars) {
7740
9011
  return value.length <= maxChars ? value : value.slice(0, maxChars);
7741
9012
  }
9013
+ async function resolveFlushPlanProcessingChainKey(workspaceRoot) {
9014
+ const lexicalRoot = path4.resolve(workspaceRoot);
9015
+ try {
9016
+ return path4.resolve(await realPathLater(lexicalRoot));
9017
+ } catch {
9018
+ return lexicalRoot;
9019
+ }
9020
+ }
7742
9021
 
7743
9022
  // src/bridge.ts
7744
9023
  import fs from "fs";
7745
- import path4 from "path";
9024
+ import path5 from "path";
7746
9025
  import { Worker } from "worker_threads";
7747
9026
  import { expandTildePath as expandTildePath2 } from "@remnic/core";
7748
9027
  var DEFAULT_HOST = "127.0.0.1";
@@ -7812,11 +9091,11 @@ function readCompatEnv(primary, legacy) {
7812
9091
  function configPathCandidates() {
7813
9092
  const envPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH");
7814
9093
  return [
7815
- ...envPath ? [path4.resolve(expandTildePath2(envPath))] : [],
7816
- path4.join(resolveHomeDir2(), ".config", "remnic", "config.json"),
7817
- path4.join(resolveHomeDir2(), ".config", "engram", "config.json"),
7818
- path4.join(process.cwd(), "remnic.config.json"),
7819
- path4.join(process.cwd(), "engram.config.json")
9094
+ ...envPath ? [path5.resolve(expandTildePath2(envPath))] : [],
9095
+ path5.join(resolveHomeDir2(), ".config", "remnic", "config.json"),
9096
+ path5.join(resolveHomeDir2(), ".config", "engram", "config.json"),
9097
+ path5.join(process.cwd(), "remnic.config.json"),
9098
+ path5.join(process.cwd(), "engram.config.json")
7820
9099
  ];
7821
9100
  }
7822
9101
  function fileExists(filePath) {
@@ -7828,8 +9107,8 @@ function fileExists(filePath) {
7828
9107
  }
7829
9108
  function isDaemonRunning() {
7830
9109
  for (const pidFile of [
7831
- path4.join(resolveHomeDir2(), ".remnic", "server.pid"),
7832
- path4.join(resolveHomeDir2(), ".engram", "server.pid")
9110
+ path5.join(resolveHomeDir2(), ".remnic", "server.pid"),
9111
+ path5.join(resolveHomeDir2(), ".engram", "server.pid")
7833
9112
  ]) {
7834
9113
  try {
7835
9114
  const pid = parseInt(fs["re"+"ad"+"Fi"+"le"+"Sync"](pidFile, "utf8").trim(), 10);
@@ -7843,7 +9122,7 @@ function isDaemonRunning() {
7843
9122
  function isDaemonServiceConfigured() {
7844
9123
  const homeDir = resolveHomeDir2();
7845
9124
  for (const segments of [...LAUNCHD_SERVICE_PATHS, ...SYSTEMD_SERVICE_PATHS]) {
7846
- if (fileExists(path4.join(homeDir, ...segments))) return true;
9125
+ if (fileExists(path5.join(homeDir, ...segments))) return true;
7847
9126
  }
7848
9127
  return false;
7849
9128
  }
@@ -7931,8 +9210,8 @@ function detectBridgeMode() {
7931
9210
  }
7932
9211
  function loadAnyToken() {
7933
9212
  const tokenPaths = [
7934
- path4.join(resolveHomeDir2(), ".remnic", "tokens.json"),
7935
- path4.join(resolveHomeDir2(), ".engram", "tokens.json")
9213
+ path5.join(resolveHomeDir2(), ".remnic", "tokens.json"),
9214
+ path5.join(resolveHomeDir2(), ".engram", "tokens.json")
7936
9215
  ];
7937
9216
  for (const tokensPath of tokenPaths) {
7938
9217
  if (!fs.existsSync(tokensPath)) continue;