@openclaw/plugin-inspector 0.3.16 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.18 - 2026-07-21
4
+
5
+ ### Highlights
6
+
7
+ - Keep compatibility capture current with OpenClaw's evolving hook, gateway, workspace-linking, blob-store, and Zod runtime surfaces.
8
+
9
+ ### Fixed
10
+
11
+ - Mock the current plugin blob-store runtime so Diffs and other trusted plugins can be captured without persistent state.
12
+ - Parse OpenClaw hook names after their source declaration became private and added a TypeScript `satisfies` constraint.
13
+ - Run synthetic gateway lifecycle hooks around ordinary probes while preserving capture indexes and report order, so `gateway_stop` teardown cannot invalidate later compatibility checks.
14
+ - Link isolated plugin workspaces to the OpenClaw checkout without npm normalizing duplicated dependency and peer declarations back to registry ranges.
15
+ - Keep mocked Zod schemas chainable through unsupported methods such as `pipe()` and `catch()`.
16
+
17
+ ## 0.3.17 - 2026-06-29
18
+
19
+ ### Fixed
20
+
21
+ - Detect deprecated session SDK read/write, file-path, and transcript helpers across source files, packaged `dist`/`build` artifacts, runtime session APIs, and dynamic SDK imports.
4
22
 
5
23
  ## 0.3.16 - 2026-06-23
6
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
@@ -194,6 +194,8 @@ function registrationReturnValue(name, args, context) {
194
194
 
195
195
  function createRuntimeContext(options) {
196
196
  const runtime = options.runtime ?? {};
197
+ const blobStores = new Map();
198
+ const syncKeyedStores = new Map();
197
199
  return {
198
200
  ...runtime,
199
201
  agent: runtime.agent ?? {},
@@ -203,11 +205,149 @@ function createRuntimeContext(options) {
203
205
  tts: runtime.tts ?? {},
204
206
  state: {
205
207
  resolveStateDir: () => options.stateDir ?? process.cwd(),
208
+ openBlobStore(storeOptions) {
209
+ const existing = blobStores.get(storeOptions.namespace);
210
+ if (existing) {
211
+ return existing;
212
+ }
213
+ const store = createBlobStoreContext(storeOptions);
214
+ blobStores.set(storeOptions.namespace, store);
215
+ return store;
216
+ },
217
+ openSyncKeyedStore({ namespace }) {
218
+ const existing = syncKeyedStores.get(namespace);
219
+ if (existing) {
220
+ return existing;
221
+ }
222
+ const store = createSyncKeyedStoreContext();
223
+ syncKeyedStores.set(namespace, store);
224
+ return store;
225
+ },
206
226
  ...(runtime.state ?? {}),
207
227
  },
208
228
  };
209
229
  }
210
230
 
231
+ function createBlobStoreContext(options) {
232
+ const values = new Map();
233
+ const entryInfo = (key, entry) => ({
234
+ key,
235
+ metadata: entry.metadata,
236
+ sizeBytes: entry.bytes.byteLength,
237
+ createdAt: entry.createdAt,
238
+ ...(entry.expiresAt === undefined ? {} : { expiresAt: entry.expiresAt }),
239
+ });
240
+ const read = (key) => {
241
+ const entry = values.get(key);
242
+ if (!entry) {
243
+ return undefined;
244
+ }
245
+ if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {
246
+ return undefined;
247
+ }
248
+ return { ...entryInfo(key, entry), bytes: Uint8Array.from(entry.bytes) };
249
+ };
250
+ const register = async (key, bytes, metadata, registerOptions) => {
251
+ const createdAt = Date.now();
252
+ const ttlMs = registerOptions?.ttlMs ?? options.defaultTtlMs;
253
+ values.set(key, {
254
+ bytes: Uint8Array.from(bytes),
255
+ metadata,
256
+ createdAt,
257
+ ...(ttlMs === undefined ? {} : { expiresAt: createdAt + ttlMs }),
258
+ });
259
+ };
260
+ return {
261
+ register,
262
+ async registerIfAbsent(key, bytes, metadata, registerOptions) {
263
+ if (values.has(key)) {
264
+ return false;
265
+ }
266
+ await register(key, bytes, metadata, registerOptions);
267
+ return true;
268
+ },
269
+ async lookup(key) {
270
+ return read(key);
271
+ },
272
+ async entries() {
273
+ return [...values.keys()].flatMap((key) => {
274
+ const entry = read(key);
275
+ if (!entry) {
276
+ return [];
277
+ }
278
+ const { bytes: _bytes, ...info } = entry;
279
+ return [info];
280
+ });
281
+ },
282
+ async delete(key) {
283
+ return values.delete(key);
284
+ },
285
+ async deleteExpiredKey(key) {
286
+ const entry = values.get(key);
287
+ if (!entry || entry.expiresAt === undefined || entry.expiresAt > Date.now()) {
288
+ return undefined;
289
+ }
290
+ values.delete(key);
291
+ return entryInfo(key, entry);
292
+ },
293
+ async deleteExpired() {
294
+ const expired = [];
295
+ for (const [key, entry] of values) {
296
+ if (entry.expiresAt === undefined || entry.expiresAt > Date.now()) {
297
+ continue;
298
+ }
299
+ values.delete(key);
300
+ expired.push(entryInfo(key, entry));
301
+ }
302
+ return expired;
303
+ },
304
+ async clear() {
305
+ values.clear();
306
+ },
307
+ };
308
+ }
309
+
310
+ function createSyncKeyedStoreContext() {
311
+ const values = new Map();
312
+ return {
313
+ register(key, value) {
314
+ values.set(key, value);
315
+ },
316
+ registerIfAbsent(key, value) {
317
+ if (values.has(key)) {
318
+ return false;
319
+ }
320
+ values.set(key, value);
321
+ return true;
322
+ },
323
+ update(key, updateValue) {
324
+ const next = updateValue(values.get(key));
325
+ if (next === undefined) {
326
+ return false;
327
+ }
328
+ values.set(key, next);
329
+ return true;
330
+ },
331
+ lookup(key) {
332
+ return values.get(key);
333
+ },
334
+ consume(key) {
335
+ const value = values.get(key);
336
+ values.delete(key);
337
+ return value;
338
+ },
339
+ delete(key) {
340
+ return values.delete(key);
341
+ },
342
+ entries() {
343
+ return [...values].map(([key, value]) => ({ key, value, createdAt: 0 }));
344
+ },
345
+ clear() {
346
+ values.clear();
347
+ },
348
+ };
349
+ }
350
+
211
351
  function createSecretContext(options) {
212
352
  const secrets = new Map(Object.entries(options.secretValues ?? {}));
213
353
  return {
@@ -724,14 +724,39 @@ function classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings,
724
724
  decisions.push({
725
725
  fixture: fixture.id,
726
726
  decision: "core-compat-adapter",
727
- seam: "session-store",
728
- action:
729
- "Keep loadSessionStore compatibility active while plugin authors migrate to row-scoped session helpers.",
727
+ seam: sdkDeprecationSeamForCode(code),
728
+ action: sdkDeprecationActionForCode(code),
730
729
  evidence: findings.map((finding) => finding.ref).join(", "),
731
730
  });
732
731
  }
733
732
  }
734
733
 
734
+ function sdkDeprecationSeamForCode(code) {
735
+ if (code === "sdk-session-file-helper") {
736
+ return "session-file";
737
+ }
738
+ if (code === "sdk-session-transcript-file-target" || code === "sdk-session-transcript-low-level") {
739
+ return "session-transcript";
740
+ }
741
+ return "session-store";
742
+ }
743
+
744
+ function sdkDeprecationActionForCode(code) {
745
+ if (code === "sdk-session-store-write") {
746
+ return "Keep whole-store session write compatibility active while plugin authors migrate to row-scoped session write helpers.";
747
+ }
748
+ if (code === "sdk-session-file-helper") {
749
+ return "Keep session file-path compatibility active while plugin authors migrate to session entry and transcript identity helpers.";
750
+ }
751
+ if (code === "sdk-session-transcript-file-target") {
752
+ return "Keep legacy transcript file target compatibility active while plugin authors migrate to structured transcript targets.";
753
+ }
754
+ if (code === "sdk-session-transcript-low-level") {
755
+ return "Keep low-level transcript write compatibility active while plugin authors migrate to structured transcript runtime helpers.";
756
+ }
757
+ return "Keep loadSessionStore compatibility active while plugin authors migrate to row-scoped session helpers.";
758
+ }
759
+
735
760
  function classifySecurityManifestCoverage({ fixture, fixtureReport, warnings, decisions }) {
736
761
  for (const securityManifest of fixtureReport.securityManifests ?? []) {
737
762
  warnings.push({
package/src/inspector.js CHANGED
@@ -95,10 +95,17 @@ export async function inspectPlugin(fixture, options = {}) {
95
95
  return emptyInspection(fixture, "missing");
96
96
  }
97
97
 
98
- const files = await listSourceFiles(sourceRoot, { includeDist: Boolean(fixture.package) });
98
+ const packageInspection = await readPackageMetadata(config, checkoutPath, sourceRoot);
99
+ const includeBuildArtifacts = shouldScanBuildArtifacts(fixture, packageInspection);
100
+ const files = await listSourceFiles(sourceRoot, {
101
+ includeBuild: includeBuildArtifacts,
102
+ includeDist: includeBuildArtifacts,
103
+ });
99
104
  if (sourceRoot !== checkoutPath) {
100
105
  files.push(...(await listSourceFiles(checkoutPath, { shallowRootOnly: true })));
101
106
  }
107
+ files.push(...packageInspection.entrypointFiles);
108
+ const sourceFiles = uniquePaths(files);
102
109
 
103
110
  const hooks = new Set();
104
111
  const registrations = new Set();
@@ -107,7 +114,7 @@ export async function inspectPlugin(fixture, options = {}) {
107
114
  const sdkImportDetails = [];
108
115
  const sdkDeprecationDetails = [];
109
116
 
110
- for (const filePath of files) {
117
+ for (const filePath of sourceFiles) {
111
118
  const text = await readFile(filePath, "utf8");
112
119
  const relativePath = path.relative(config.rootDir ?? process.cwd(), filePath);
113
120
  const sourceInspection = inspectSourceText(text, relativePath);
@@ -129,8 +136,6 @@ export async function inspectPlugin(fixture, options = {}) {
129
136
  }
130
137
 
131
138
  const manifestInspection = await readManifestContracts(config, checkoutPath, sourceRoot);
132
- const packageInspection = await readPackageMetadata(config, checkoutPath, sourceRoot);
133
-
134
139
  return {
135
140
  id: fixture.id,
136
141
  status: "ok",
@@ -146,7 +151,7 @@ export async function inspectPlugin(fixture, options = {}) {
146
151
  packageEntrypoints: packageInspection.entrypoints,
147
152
  sdkImports: uniqueDetails(sdkImportDetails),
148
153
  sdkDeprecations: uniqueSdkDeprecations(sdkDeprecationDetails),
149
- sourceFiles: files.map((filePath) => path.relative(config.rootDir ?? process.cwd(), filePath)).sort(),
154
+ sourceFiles: sourceFiles.map((filePath) => path.relative(config.rootDir ?? process.cwd(), filePath)).sort(),
150
155
  };
151
156
  }
152
157
 
@@ -418,18 +423,23 @@ async function readPackageMetadata(config, checkoutPath, sourceRoot) {
418
423
  const files = [];
419
424
  const errors = [];
420
425
  const entrypoints = new Set();
426
+ const entrypointFiles = new Set();
421
427
 
422
428
  for (const packageFile of packageFiles) {
423
429
  const relativePath = path.relative(config.rootDir ?? process.cwd(), packageFile);
424
430
  files.push(relativePath);
425
431
  try {
426
432
  const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
427
- collectEntrypoint(entrypoints, packageJson.main);
428
- collectEntrypoint(entrypoints, packageJson.module);
429
- collectEntrypoint(entrypoints, packageJson.openclaw?.entry);
430
- collectEntrypoint(entrypoints, packageJson.openclaw?.entrypoint);
431
- collectEntrypoint(entrypoints, packageJson.exports?.["."]?.import);
432
- collectEntrypoint(entrypoints, packageJson.exports?.["."]?.default);
433
+ const packageDir = path.dirname(packageFile);
434
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.main);
435
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.module);
436
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.entry);
437
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.entrypoint);
438
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.setupEntry);
439
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.exports?.["."]?.import);
440
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.exports?.["."]?.default);
441
+ collectEntrypoints(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.extensions);
442
+ collectEntrypoints(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.runtimeExtensions);
433
443
  } catch {
434
444
  errors.push(`${relativePath}: invalid JSON`);
435
445
  }
@@ -439,13 +449,58 @@ async function readPackageMetadata(config, checkoutPath, sourceRoot) {
439
449
  files: files.sort(),
440
450
  errors,
441
451
  entrypoints: [...entrypoints].sort(),
452
+ entrypointFiles: [...entrypointFiles].sort(),
442
453
  };
443
454
  }
444
455
 
445
- function collectEntrypoint(entrypoints, value) {
456
+ function collectEntrypoints(entrypoints, entrypointFiles, packageDir, values) {
457
+ if (!Array.isArray(values)) {
458
+ return;
459
+ }
460
+ for (const value of values) {
461
+ collectEntrypoint(entrypoints, entrypointFiles, packageDir, value);
462
+ }
463
+ }
464
+
465
+ function collectEntrypoint(entrypoints, entrypointFiles, packageDir, value) {
446
466
  if (typeof value === "string" && value.length > 0) {
447
467
  entrypoints.add(value);
468
+ for (const candidate of entrypointCandidates(packageDir, value)) {
469
+ if (existsSync(candidate) && isSourceFile(path.basename(candidate), candidate.split(path.sep).join("/"))) {
470
+ entrypointFiles.add(candidate);
471
+ return;
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ function entrypointCandidates(packageDir, specifier) {
478
+ const resolved = path.resolve(packageDir, specifier);
479
+ if (path.extname(resolved)) {
480
+ return [resolved];
481
+ }
482
+ return [
483
+ resolved,
484
+ `${resolved}.js`,
485
+ `${resolved}.mjs`,
486
+ `${resolved}.cjs`,
487
+ `${resolved}.ts`,
488
+ path.join(resolved, "index.js"),
489
+ path.join(resolved, "index.mjs"),
490
+ path.join(resolved, "index.cjs"),
491
+ path.join(resolved, "index.ts"),
492
+ ];
493
+ }
494
+
495
+ function uniquePaths(paths) {
496
+ return [...new Set(paths)];
497
+ }
498
+
499
+ function shouldScanBuildArtifacts(fixture, packageInspection) {
500
+ if (fixture.package) {
501
+ return true;
448
502
  }
503
+ return packageInspection.entrypoints.some((entrypoint) => /(^|\/)(?:dist|build)\//.test(entrypoint));
449
504
  }
450
505
 
451
506
  async function listSourceFiles(root, options = {}) {
@@ -484,7 +539,7 @@ function shouldSkipDir(name, normalizedPath, options = {}) {
484
539
  return (
485
540
  name === "node_modules" ||
486
541
  (!options.includeDist && name === "dist") ||
487
- name === "build" ||
542
+ (!options.includeBuild && name === "build") ||
488
543
  name === "coverage" ||
489
544
  name === ".git" ||
490
545
  name === "test" ||
package/src/issues.js CHANGED
@@ -42,6 +42,10 @@ export const knownIssueCodes = new Set([
42
42
  "reserved-sdk-import",
43
43
  "security-manifest-schema-unavailable",
44
44
  "sdk-load-session-store",
45
+ "sdk-session-file-helper",
46
+ "sdk-session-store-write",
47
+ "sdk-session-transcript-file-target",
48
+ "sdk-session-transcript-low-level",
45
49
  "sdk-export-missing",
46
50
  "unrecognized-security-manifest",
47
51
  ]);
@@ -124,6 +128,58 @@ export const issueMetadataByCode = {
124
128
  ],
125
129
  ),
126
130
  },
131
+ "sdk-session-store-write": {
132
+ severity: "P2",
133
+ owner: "core",
134
+ decision: "core-compat-adapter",
135
+ title: "deprecated whole-store session write helper is still used",
136
+ authorRemediation: migrationRemediation(
137
+ "Replace deprecated whole-store session writes with row-scoped session helpers.",
138
+ [
139
+ "Use patchSessionEntry(...) when updating fields on an existing session entry.",
140
+ "Use upsertSessionEntry(...) when replacing or creating a session entry.",
141
+ ],
142
+ ),
143
+ },
144
+ "sdk-session-file-helper": {
145
+ severity: "P2",
146
+ owner: "core",
147
+ decision: "core-compat-adapter",
148
+ title: "deprecated session file-path helper is still used",
149
+ authorRemediation: migrationRemediation(
150
+ "Replace deprecated session file-path helpers with session entry and transcript identity APIs.",
151
+ [
152
+ "Use getSessionEntry(...) to read session metadata by agent/session identity.",
153
+ "Use patchSessionEntry(...) or upsertSessionEntry(...) to persist session metadata.",
154
+ ],
155
+ ),
156
+ },
157
+ "sdk-session-transcript-file-target": {
158
+ severity: "P2",
159
+ owner: "core",
160
+ decision: "core-compat-adapter",
161
+ title: "deprecated transcript file target helper is still used",
162
+ authorRemediation: migrationRemediation(
163
+ "Replace legacy transcript file targets with public transcript identity or target helpers.",
164
+ [
165
+ "Use resolveSessionTranscriptIdentity(...) when you only need public session identity.",
166
+ "Use resolveSessionTranscriptTarget(...) when you need a structured transcript operation target.",
167
+ ],
168
+ ),
169
+ },
170
+ "sdk-session-transcript-low-level": {
171
+ severity: "P2",
172
+ owner: "core",
173
+ decision: "core-compat-adapter",
174
+ title: "deprecated low-level transcript helper is still used",
175
+ authorRemediation: migrationRemediation(
176
+ "Replace low-level transcript writes with the structured transcript runtime helpers.",
177
+ [
178
+ "Use appendSessionTranscriptMessageByIdentity(...) for transcript appends.",
179
+ "Use publishSessionTranscriptUpdateByIdentity(...) for transcript update notifications.",
180
+ ],
181
+ ),
182
+ },
127
183
  "sdk-export-missing": {
128
184
  severity: "P1",
129
185
  owner: "core",
@@ -568,6 +624,10 @@ function issueClassFor(code, options) {
568
624
  "legacy-root-sdk-import",
569
625
  "provider-auth-env-vars",
570
626
  "sdk-load-session-store",
627
+ "sdk-session-file-helper",
628
+ "sdk-session-store-write",
629
+ "sdk-session-transcript-file-target",
630
+ "sdk-session-transcript-low-level",
571
631
  ].includes(code)
572
632
  ) {
573
633
  return "deprecation-warning";
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { writeJsonFileAtomic } from "./json-file.js";
6
+
7
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
8
+ const specifier = process.argv[2];
9
+ await linkOpenClawWorkspace(path.resolve(process.cwd(), "package.json"), specifier);
10
+ }
11
+
12
+ export async function linkOpenClawWorkspace(packageJsonPath, specifier) {
13
+ if (typeof specifier !== "string" || !specifier.startsWith("file:")) {
14
+ throw new TypeError("link-openclaw-workspace requires a file: dependency specifier");
15
+ }
16
+
17
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
18
+ let linked = false;
19
+ for (const section of ["dependencies", "optionalDependencies"]) {
20
+ if (Object.hasOwn(packageJson[section] ?? {}, "openclaw")) {
21
+ packageJson[section].openclaw = specifier;
22
+ linked = true;
23
+ }
24
+ }
25
+
26
+ if (!linked) {
27
+ packageJson.dependencies ??= {};
28
+ packageJson.dependencies.openclaw = specifier;
29
+ }
30
+
31
+ await writeJsonFileAtomic(packageJsonPath, packageJson);
32
+ }
@@ -37,7 +37,7 @@ export async function readOpenClawTargetSurface(options = {}) {
37
37
  const registrySource = await readFile(registryPath, "utf8");
38
38
  const compatRecordEntries = parseCompatRecordEntries(registrySource);
39
39
  const hookTypesSource = existsSync(hookTypesPath) ? await readFile(hookTypesPath, "utf8") : "";
40
- const hookNames = hookTypesSource ? parseExportedStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
40
+ const hookNames = hookTypesSource ? parseConstStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
41
41
  const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
42
42
  const apiRegistrars = apiBuilderSource ? parseApiRegistrars(apiBuilderSource) : [];
43
43
  const manifestTypesSource = existsSync(manifestTypesPath) ? await readFile(manifestTypesPath, "utf8") : "";
@@ -198,6 +198,15 @@ export function parsePluginSdkExports(packageJson) {
198
198
 
199
199
  export function parseExportedStringArray(source, exportName) {
200
200
  const match = source.match(new RegExp(`export\\s+const\\s+${exportName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
201
+ return parseStringArrayMatch(match);
202
+ }
203
+
204
+ function parseConstStringArray(source, constName) {
205
+ const match = source.match(new RegExp(`(?:export\\s+)?const\\s+${constName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
206
+ return parseStringArrayMatch(match);
207
+ }
208
+
209
+ function parseStringArrayMatch(match) {
201
210
  if (!match) {
202
211
  return [];
203
212
  }
@@ -1,16 +1,53 @@
1
- const loadSessionStoreReplacement =
1
+ const sessionStoreReadReplacement =
2
2
  "getSessionEntry(...) / listSessionEntries(...) for reads and patchSessionEntry(...) / upsertSessionEntry(...) for writes";
3
+ const sessionStoreWriteReplacement = "patchSessionEntry(...) / upsertSessionEntry(...) for row-scoped writes";
4
+ const sessionFileReplacement = "session entries and transcript identity helpers instead of persisted file paths";
5
+ const sessionTranscriptReplacement =
6
+ "resolveSessionTranscriptTarget(...), appendSessionTranscriptMessageByIdentity(...), and publishSessionTranscriptUpdateByIdentity(...)";
3
7
 
4
- const loadSessionStoreSpecifiers = new Set([
8
+ const sdkSessionSpecifiers = new Set([
5
9
  "openclaw/plugin-sdk/config-runtime",
10
+ "openclaw/plugin-sdk/mattermost",
11
+ "openclaw/plugin-sdk/agent-harness-runtime",
6
12
  "openclaw/plugin-sdk/session-store-runtime",
13
+ "openclaw/plugin-sdk/session-transcript-runtime",
7
14
  ]);
8
15
 
9
16
  export const pluginSdkDeprecationRules = [
10
17
  {
11
18
  code: "sdk-load-session-store",
19
+ symbols: new Set(["loadSessionStore"]),
12
20
  title: "deprecated whole-store session helper is still used",
13
- replacement: loadSessionStoreReplacement,
21
+ replacement: sessionStoreReadReplacement,
22
+ message: (symbol, replacement) => `${symbol} keeps the legacy whole-store session shape; use ${replacement}.`,
23
+ },
24
+ {
25
+ code: "sdk-session-store-write",
26
+ symbols: new Set(["saveSessionStore", "updateSessionStore"]),
27
+ title: "deprecated whole-store session write helper is still used",
28
+ replacement: sessionStoreWriteReplacement,
29
+ message: (symbol, replacement) => `${symbol} writes the legacy whole-store session shape; use ${replacement}.`,
30
+ },
31
+ {
32
+ code: "sdk-session-file-helper",
33
+ symbols: new Set(["resolveSessionFilePath", "resolveAndPersistSessionFile"]),
34
+ title: "deprecated session file-path helper is still used",
35
+ replacement: sessionFileReplacement,
36
+ message: (symbol, replacement) => `${symbol} depends on legacy session transcript file paths; use ${replacement}.`,
37
+ },
38
+ {
39
+ code: "sdk-session-transcript-file-target",
40
+ symbols: new Set(["resolveSessionTranscriptLegacyFileTarget"]),
41
+ title: "deprecated transcript file target helper is still used",
42
+ replacement: "resolveSessionTranscriptTarget(...) or resolveSessionTranscriptIdentity(...)",
43
+ message: (symbol, replacement) => `${symbol} exposes legacy transcript file targets; use ${replacement}.`,
44
+ },
45
+ {
46
+ code: "sdk-session-transcript-low-level",
47
+ symbols: new Set(["appendSessionTranscriptMessage", "emitSessionTranscriptUpdate"]),
48
+ title: "deprecated low-level transcript helper is still used",
49
+ replacement: sessionTranscriptReplacement,
50
+ message: (symbol, replacement) => `${symbol} bypasses the structured transcript runtime surface; use ${replacement}.`,
14
51
  },
15
52
  ];
16
53
 
@@ -18,9 +55,7 @@ export function inspectSdkDeprecations(text, filePath = "source.js", rules = plu
18
55
  const findings = [];
19
56
 
20
57
  for (const rule of rules) {
21
- if (rule.code === "sdk-load-session-store") {
22
- collectLoadSessionStoreDeprecations(findings, { text, filePath, rule });
23
- }
58
+ collectSdkHelperDeprecations(findings, { text, filePath, rule });
24
59
  }
25
60
 
26
61
  return uniqueFindings(findings)
@@ -28,12 +63,13 @@ export function inspectSdkDeprecations(text, filePath = "source.js", rules = plu
28
63
  .map(({ offset, ...finding }) => finding);
29
64
  }
30
65
 
31
- function collectLoadSessionStoreDeprecations(findings, context) {
66
+ function collectSdkHelperDeprecations(findings, context) {
32
67
  collectNamedImportDeprecations(findings, context);
33
68
  collectNamedReexportDeprecations(findings, context);
34
69
  collectNamedRequireDeprecations(findings, context);
35
70
  collectNamespaceUsageDeprecations(findings, context);
36
71
  collectNamespaceRequireDeprecations(findings, context);
72
+ collectDynamicImportNamespaceDeprecations(findings, context);
37
73
  collectRuntimeUsageDeprecations(findings, context);
38
74
  collectRuntimeAliasUsageDeprecations(findings, context);
39
75
  }
@@ -43,16 +79,17 @@ function collectNamedImportDeprecations(findings, context) {
43
79
  /\bimport\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?{([^}]+)}\s*from\s*["'`]([^"'`]+)["'`]/g;
44
80
  for (const match of context.text.matchAll(regex)) {
45
81
  const specifier = match[2];
46
- if (!loadSessionStoreSpecifiers.has(specifier)) {
82
+ if (!sdkSessionSpecifiers.has(specifier)) {
47
83
  continue;
48
84
  }
49
85
  for (const binding of parseNamedBindings(match[1])) {
50
- if (binding.exported !== "loadSessionStore") {
86
+ if (!context.rule.symbols.has(binding.exported)) {
51
87
  continue;
52
88
  }
53
89
  findings.push(
54
90
  buildFinding(context.rule, {
55
- surface: `${specifier} import`,
91
+ surface: `${specifier} ${binding.exported} import`,
92
+ symbol: binding.exported,
56
93
  sourceText: context.text,
57
94
  filePath: context.filePath,
58
95
  offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
@@ -66,16 +103,17 @@ function collectNamedReexportDeprecations(findings, context) {
66
103
  const regex = /\bexport\s*{([^}]+)}\s*from\s*["'`]([^"'`]+)["'`]/g;
67
104
  for (const match of context.text.matchAll(regex)) {
68
105
  const specifier = match[2];
69
- if (!loadSessionStoreSpecifiers.has(specifier)) {
106
+ if (!sdkSessionSpecifiers.has(specifier)) {
70
107
  continue;
71
108
  }
72
109
  for (const binding of parseNamedBindings(match[1])) {
73
- if (binding.exported !== "loadSessionStore") {
110
+ if (!context.rule.symbols.has(binding.exported)) {
74
111
  continue;
75
112
  }
76
113
  findings.push(
77
114
  buildFinding(context.rule, {
78
- surface: `${specifier} re-export`,
115
+ surface: `${specifier} ${binding.exported} re-export`,
116
+ symbol: binding.exported,
79
117
  sourceText: context.text,
80
118
  filePath: context.filePath,
81
119
  offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
@@ -89,16 +127,17 @@ function collectNamedRequireDeprecations(findings, context) {
89
127
  const regex = /\b(?:const|let|var)\s+{([^}]+)}\s*=\s*require\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
90
128
  for (const match of context.text.matchAll(regex)) {
91
129
  const specifier = match[2];
92
- if (!loadSessionStoreSpecifiers.has(specifier)) {
130
+ if (!sdkSessionSpecifiers.has(specifier)) {
93
131
  continue;
94
132
  }
95
133
  for (const binding of parseNamedBindings(match[1], { aliasSeparator: ":" })) {
96
- if (binding.exported !== "loadSessionStore") {
134
+ if (!context.rule.symbols.has(binding.exported)) {
97
135
  continue;
98
136
  }
99
137
  findings.push(
100
138
  buildFinding(context.rule, {
101
- surface: `${specifier} require`,
139
+ surface: `${specifier} ${binding.exported} require`,
140
+ symbol: binding.exported,
102
141
  sourceText: context.text,
103
142
  filePath: context.filePath,
104
143
  offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
@@ -109,21 +148,24 @@ function collectNamedRequireDeprecations(findings, context) {
109
148
  }
110
149
 
111
150
  function collectMemberCallDeprecations(findings, context, options) {
112
- forEachMethodCall(context.text, "loadSessionStore", (offset) => {
113
- // Normalize transparent parentheses and optional-chained member links before matching.
114
- const receiver = readNormalizedCallReceiver(context.text, offset);
115
- if (!receiver || !options.receiverMatcher(receiver)) {
116
- return;
117
- }
118
- findings.push(
119
- buildFinding(context.rule, {
120
- surface: options.surface,
121
- sourceText: context.text,
122
- filePath: context.filePath,
123
- offset,
124
- }),
125
- );
126
- });
151
+ for (const symbol of context.rule.symbols) {
152
+ forEachMethodCall(context.text, symbol, (offset) => {
153
+ // Normalize transparent parentheses and optional-chained member links before matching.
154
+ const receiver = readNormalizedCallReceiver(context.text, offset);
155
+ if (!receiver || !options.receiverMatcher(receiver)) {
156
+ return;
157
+ }
158
+ findings.push(
159
+ buildFinding(context.rule, {
160
+ surface: `${options.surface} ${symbol}`,
161
+ symbol,
162
+ sourceText: context.text,
163
+ filePath: context.filePath,
164
+ offset,
165
+ }),
166
+ );
167
+ });
168
+ }
127
169
  }
128
170
 
129
171
  function forEachMethodCall(text, methodName, visit) {
@@ -259,7 +301,11 @@ function isIdentifierBoundary(text, offset) {
259
301
  }
260
302
 
261
303
  function isRuntimeSessionReceiver(receiver) {
262
- return /^(?:[A-Za-z_$][A-Za-z0-9_$]*|this)\.runtime\.agent\.session$/.test(receiver);
304
+ return (
305
+ /(?:^|\.)(?:[A-Za-z_$][A-Za-z0-9_$]*|this)\.runtime\.agent\.session$/.test(receiver) ||
306
+ /^(?:runtime|[A-Za-z_$][A-Za-z0-9_$]*Runtime)\.agent\.session$/.test(receiver) ||
307
+ /^(?:agentRuntime|[A-Za-z_$][A-Za-z0-9_$]*AgentRuntime)\.session$/.test(receiver)
308
+ );
263
309
  }
264
310
 
265
311
  function collectNamespaceUsageDeprecations(findings, context) {
@@ -267,7 +313,7 @@ function collectNamespaceUsageDeprecations(findings, context) {
267
313
  for (const match of context.text.matchAll(regex)) {
268
314
  const local = match[1];
269
315
  const specifier = match[2];
270
- if (!loadSessionStoreSpecifiers.has(specifier)) {
316
+ if (!sdkSessionSpecifiers.has(specifier)) {
271
317
  continue;
272
318
  }
273
319
  collectMemberCallDeprecations(findings, context, {
@@ -282,7 +328,7 @@ function collectNamespaceRequireDeprecations(findings, context) {
282
328
  for (const match of context.text.matchAll(regex)) {
283
329
  const local = match[1];
284
330
  const specifier = match[2];
285
- if (!loadSessionStoreSpecifiers.has(specifier)) {
331
+ if (!sdkSessionSpecifiers.has(specifier)) {
286
332
  continue;
287
333
  }
288
334
  collectMemberCallDeprecations(findings, context, {
@@ -292,6 +338,22 @@ function collectNamespaceRequireDeprecations(findings, context) {
292
338
  }
293
339
  }
294
340
 
341
+ function collectDynamicImportNamespaceDeprecations(findings, context) {
342
+ const regex =
343
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?import\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
344
+ for (const match of context.text.matchAll(regex)) {
345
+ const local = match[1];
346
+ const specifier = match[2];
347
+ if (!sdkSessionSpecifiers.has(specifier)) {
348
+ continue;
349
+ }
350
+ collectMemberCallDeprecations(findings, context, {
351
+ receiverMatcher: (receiver) => receiver === local,
352
+ surface: `${specifier} dynamic import namespace access`,
353
+ });
354
+ }
355
+ }
356
+
295
357
  function collectRuntimeUsageDeprecations(findings, context) {
296
358
  collectMemberCallDeprecations(findings, context, {
297
359
  receiverMatcher: isRuntimeSessionReceiver,
@@ -509,10 +571,11 @@ function buildFinding(rule, details) {
509
571
  const refLine = lineForOffset(details.sourceText, details.offset);
510
572
  return {
511
573
  code: rule.code,
574
+ symbol: details.symbol,
512
575
  surface: details.surface,
513
576
  replacement: rule.replacement,
514
577
  ref: `${details.filePath}:${refLine}`,
515
- message: `loadSessionStore keeps the legacy whole-store session shape; use ${rule.replacement}.`,
578
+ message: rule.message(details.symbol, rule.replacement),
516
579
  offset: details.offset,
517
580
  };
518
581
  }
@@ -520,7 +583,7 @@ function buildFinding(rule, details) {
520
583
  function uniqueFindings(findings) {
521
584
  const byKey = new Map();
522
585
  for (const finding of findings) {
523
- byKey.set(`${finding.code}:${finding.surface}:${finding.ref}`, finding);
586
+ byKey.set(`${finding.code}:${finding.symbol}:${finding.surface}:${finding.ref}`, finding);
524
587
  }
525
588
  return [...byKey.values()];
526
589
  }
package/src/sdk-mock.js CHANGED
@@ -968,11 +968,11 @@ function createSchema(defaultValue, shape) {
968
968
  },
969
969
  };
970
970
  return new Proxy(schema, {
971
- get(target, property) {
971
+ get(target, property, receiver) {
972
972
  if (property in target) {
973
973
  return target[property];
974
974
  }
975
- return () => target;
975
+ return () => receiver;
976
976
  },
977
977
  });
978
978
  }
@@ -236,6 +236,11 @@ export const syntheticRegistrationExecutionProfiles = {
236
236
  callableProperties: [],
237
237
  reason: "session actions are captured as registration metadata before session runtime execution",
238
238
  },
239
+ registerSessionCatalog: {
240
+ mode: "metadata-only",
241
+ callableProperties: [],
242
+ reason: "session catalogs are captured as registration metadata before catalog runtime execution",
243
+ },
239
244
  registerService: {
240
245
  mode: "lifecycle-opt-in",
241
246
  callableProperties: ["start", "stop", "dispose"],
@@ -591,23 +596,37 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
591
596
  const hookContexts = options.hookContexts ?? defaultSyntheticHookContexts;
592
597
  const captured = capture.captured ?? [];
593
598
  const retained = new Map((capture.retained ?? []).map((item) => [item.captureIndex, item]));
594
- const results = [];
599
+ const resultsByCaptureIndex = new Map();
600
+ const executionEntries = captured
601
+ .map((entry, captureIndex) => ({ entry, captureIndex }))
602
+ .sort(
603
+ (left, right) =>
604
+ syntheticLifecyclePhase(left.entry) - syntheticLifecyclePhase(right.entry) ||
605
+ left.captureIndex - right.captureIndex,
606
+ );
595
607
 
596
- for (let index = 0; index < captured.length; index += 1) {
597
- const entry = captured[index];
598
- const retainedEntry = retained.get(index);
608
+ for (const { entry, captureIndex } of executionEntries) {
609
+ const retainedEntry = retained.get(captureIndex);
599
610
  if (!retainedEntry) {
600
- results.push(blockedResult(entry, index, "handler retention was not enabled"));
611
+ resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, "handler retention was not enabled")]);
601
612
  continue;
602
613
  }
603
614
  if (entry.kind === "hook") {
604
- results.push(await runHookProbe(entry, retainedEntry, index, { hookEvents, hookContexts }));
615
+ resultsByCaptureIndex.set(captureIndex, [
616
+ await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }),
617
+ ]);
605
618
  continue;
606
619
  }
607
620
  if (entry.kind === "registration") {
608
- results.push(...(await runRegistrationProbes(entry, retainedEntry, index, options)));
621
+ resultsByCaptureIndex.set(
622
+ captureIndex,
623
+ await runRegistrationProbes(entry, retainedEntry, captureIndex, options),
624
+ );
609
625
  }
610
626
  }
627
+ // Execute in lifecycle order, but keep report rows and captureIndex values
628
+ // stable for downstream consumers that compare artifacts across runs.
629
+ const results = captured.flatMap((_, captureIndex) => resultsByCaptureIndex.get(captureIndex) ?? []);
611
630
 
612
631
  return {
613
632
  entrypoint: capture.entrypoint,
@@ -622,6 +641,19 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
622
641
  };
623
642
  }
624
643
 
644
+ function syntheticLifecyclePhase(entry) {
645
+ if (entry.kind !== "hook") {
646
+ return 1;
647
+ }
648
+ if (entry.name === "gateway_start") {
649
+ return 0;
650
+ }
651
+ if (entry.name === "gateway_stop") {
652
+ return 2;
653
+ }
654
+ return 1;
655
+ }
656
+
625
657
  export function renderSyntheticProbeMarkdown(plan, options = {}) {
626
658
  return [
627
659
  `# ${options.title ?? "Plugin Inspector Synthetic Probes"}`,
@@ -241,9 +241,10 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
241
241
  });
242
242
 
243
243
  if (requiredCapabilities.includes("target-openclaw-link")) {
244
+ const targetSpecifier = `file:${targetOpenClawWorkspacePath(settings, fixtureId, targetOpenClawPath)}`;
244
245
  steps.push({
245
246
  kind: "link-openclaw",
246
- command: `${shellQuote(packageManager)} pkg set ${shellQuote(`dependencies.openclaw=file:${targetOpenClawWorkspacePath(settings, fixtureId, targetOpenClawPath)}`)}`,
247
+ command: `node ${shellQuote(helperScript(settings, workspacePath, settings.linkOpenClawWorkspaceScript, "link-openclaw-workspace-cli.js"))} ${shellQuote(targetSpecifier)}`,
247
248
  cwd: workspacePath,
248
249
  reason: "link the plugin's openclaw peer dependency to the target checkout under test",
249
250
  });
@@ -325,6 +326,7 @@ function workspaceSettings(options) {
325
326
  return {
326
327
  captureScript: options.captureScript ?? defaultWorkspacePlanOptions.captureScript,
327
328
  defaultTargetOpenClawWorkspacePath: options.defaultTargetOpenClawWorkspacePath ?? "../../../openclaw",
329
+ linkOpenClawWorkspaceScript: options.linkOpenClawWorkspaceScript,
328
330
  optInEnv: options.optInEnv ?? defaultWorkspacePlanOptions.optInEnv,
329
331
  resultsRoot: repoRelative(options.resultsRoot ?? defaultWorkspacePlanOptions.resultsRoot),
330
332
  rootDir: path.resolve(options.rootDir ?? process.cwd()),