@remembrance-ai/opencode-plugin 0.1.63 → 0.1.66

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/opencode.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "command": [
7
7
  "npx",
8
8
  "-y",
9
- "@remembrance-ai/mcp-server@0.1.63"
9
+ "@remembrance-ai/mcp-server@0.1.66"
10
10
  ],
11
11
  "enabled": true,
12
12
  "environment": {
@@ -16,6 +16,6 @@
16
16
  }
17
17
  },
18
18
  "plugin": [
19
- "@remembrance-ai/opencode-plugin@0.1.63"
19
+ "@remembrance-ai/opencode-plugin@0.1.66"
20
20
  ]
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remembrance-ai/opencode-plugin",
3
- "version": "0.1.63",
3
+ "version": "0.1.66",
4
4
  "license": "Apache-2.0",
5
5
  "description": "opencode plugin for Remembrance session hooks, tool observation, and contribution nudges.",
6
6
  "type": "module",
@@ -3078,6 +3078,7 @@ async function registerHookIdentity(identity, fetchImpl, access, signal) {
3078
3078
  // Registration remains fail-open; an existing same-scope key can still use
3079
3079
  // the legacy proof while new organization claims require the binding.
3080
3080
  }
3081
+ if (signal.aborted) return false;
3081
3082
  const signedAt = new Date().toISOString();
3082
3083
  const publicKeyHash = `sha256:${createHash("sha256")
3083
3084
  .update(identity.public_key)
@@ -3118,7 +3119,7 @@ async function registerHookIdentity(identity, fetchImpl, access, signal) {
3118
3119
  signal,
3119
3120
  },
3120
3121
  );
3121
- return response?.ok === true;
3122
+ return !signal.aborted && response?.ok === true;
3122
3123
  }
3123
3124
 
3124
3125
  function principalSessionCachePath(
@@ -3287,90 +3288,101 @@ async function warmPrincipalSessionUncached(
3287
3288
  env,
3288
3289
  ) {
3289
3290
  const controller = new AbortController();
3290
- const timeout = setTimeout(
3291
- () => controller.abort(),
3292
- PRINCIPAL_SESSION_TIMEOUT_MS,
3293
- );
3294
- try {
3295
- if (
3296
- !(await registerHookIdentity(
3297
- identity,
3291
+ const operation = (async () => {
3292
+ try {
3293
+ if (
3294
+ !(await registerHookIdentity(
3295
+ identity,
3296
+ fetchImpl,
3297
+ apiAccess,
3298
+ controller.signal,
3299
+ ))
3300
+ ) {
3301
+ return null;
3302
+ }
3303
+ if (controller.signal.aborted) return null;
3304
+ const memberLinkToken = apiAccess.memberLinkToken;
3305
+ const profileKey = createHash("sha256")
3306
+ .update(
3307
+ [
3308
+ identity.key_id,
3309
+ normalizedRuntime,
3310
+ normalizedHostSurface,
3311
+ "plugin_hook",
3312
+ ].join(":"),
3313
+ "utf8",
3314
+ )
3315
+ .digest("base64url");
3316
+ const challengePayload = {
3317
+ action: "challenge",
3318
+ provider: identity.provider,
3319
+ key_id: identity.key_id,
3320
+ runtime_profile: {
3321
+ runtime: normalizedRuntime,
3322
+ surface: "plugin_hook",
3323
+ host_surface: normalizedHostSurface,
3324
+ client_name: runtimeDisplayName(normalizedRuntime),
3325
+ client_version: safeText(clientVersion ?? "unknown", 64),
3326
+ runtime_version: safeText(hostVersion ?? "", 64) || null,
3327
+ profile_key: `rpf_${profileKey}`,
3328
+ },
3329
+ ...(memberLinkToken ? { member_link_token: memberLinkToken } : {}),
3330
+ };
3331
+ let challenge = await postPrincipalSession(
3332
+ challengePayload,
3298
3333
  fetchImpl,
3299
3334
  apiAccess,
3300
3335
  controller.signal,
3301
- ))
3302
- ) {
3303
- return null;
3304
- }
3305
- const memberLinkToken = apiAccess.memberLinkToken;
3306
- const profileKey = createHash("sha256")
3307
- .update(
3308
- [
3309
- identity.key_id,
3310
- normalizedRuntime,
3311
- normalizedHostSurface,
3312
- "plugin_hook",
3313
- ].join(":"),
3314
- "utf8",
3315
- )
3316
- .digest("base64url");
3317
- const challengePayload = {
3318
- action: "challenge",
3319
- provider: identity.provider,
3320
- key_id: identity.key_id,
3321
- runtime_profile: {
3322
- runtime: normalizedRuntime,
3323
- surface: "plugin_hook",
3324
- host_surface: normalizedHostSurface,
3325
- client_name: runtimeDisplayName(normalizedRuntime),
3326
- client_version: safeText(clientVersion ?? "unknown", 64),
3327
- runtime_version: safeText(hostVersion ?? "", 64) || null,
3328
- profile_key: `rpf_${profileKey}`,
3329
- },
3330
- ...(memberLinkToken ? { member_link_token: memberLinkToken } : {}),
3331
- };
3332
- let challenge = await postPrincipalSession(
3333
- challengePayload,
3334
- fetchImpl,
3335
- apiAccess,
3336
- controller.signal,
3337
- );
3338
- if (!challenge && memberLinkToken) {
3339
- challenge = await postPrincipalSession(
3340
- { ...challengePayload, member_link_token: undefined },
3336
+ );
3337
+ if (!challenge && memberLinkToken) {
3338
+ challenge = await postPrincipalSession(
3339
+ { ...challengePayload, member_link_token: undefined },
3340
+ fetchImpl,
3341
+ apiAccess,
3342
+ controller.signal,
3343
+ );
3344
+ }
3345
+ if (controller.signal.aborted) return null;
3346
+ if (!challenge?.challenge_id || !challenge?.signing_payload) return null;
3347
+ const signature = signPayload(
3348
+ null,
3349
+ Buffer.from(challenge.signing_payload),
3350
+ createPrivateKey(identity.private_key),
3351
+ ).toString("base64url");
3352
+ const session = await postPrincipalSession(
3353
+ {
3354
+ action: "exchange",
3355
+ provider: identity.provider,
3356
+ key_id: identity.key_id,
3357
+ challenge_id: challenge.challenge_id,
3358
+ signature,
3359
+ },
3341
3360
  fetchImpl,
3342
3361
  apiAccess,
3343
3362
  controller.signal,
3344
3363
  );
3364
+ if (controller.signal.aborted) return null;
3365
+ if (!session?.session_token || !session?.expires_at) return null;
3366
+ const stored = {
3367
+ token: session.session_token,
3368
+ expires_at: session.expires_at,
3369
+ member_linked: session.member_linked === true,
3370
+ };
3371
+ writeHookPrincipalSession(warmupKey, stored);
3372
+ return stored;
3373
+ } catch {
3374
+ return null;
3345
3375
  }
3346
- if (!challenge?.challenge_id || !challenge?.signing_payload) return null;
3347
- const signature = signPayload(
3348
- null,
3349
- Buffer.from(challenge.signing_payload),
3350
- createPrivateKey(identity.private_key),
3351
- ).toString("base64url");
3352
- const session = await postPrincipalSession(
3353
- {
3354
- action: "exchange",
3355
- provider: identity.provider,
3356
- key_id: identity.key_id,
3357
- challenge_id: challenge.challenge_id,
3358
- signature,
3359
- },
3360
- fetchImpl,
3361
- apiAccess,
3362
- controller.signal,
3363
- );
3364
- if (!session?.session_token || !session?.expires_at) return null;
3365
- const stored = {
3366
- token: session.session_token,
3367
- expires_at: session.expires_at,
3368
- member_linked: session.member_linked === true,
3369
- };
3370
- writeHookPrincipalSession(warmupKey, stored);
3371
- return stored;
3372
- } catch {
3373
- return null;
3376
+ })();
3377
+ let timeout;
3378
+ const deadline = new Promise((resolve) => {
3379
+ timeout = setTimeout(() => {
3380
+ controller.abort();
3381
+ resolve(null);
3382
+ }, PRINCIPAL_SESSION_TIMEOUT_MS);
3383
+ });
3384
+ try {
3385
+ return await Promise.race([operation, deadline]);
3374
3386
  } finally {
3375
3387
  clearTimeout(timeout);
3376
3388
  }
@@ -10702,8 +10702,9 @@ function localPluginLifecycleHealth(options) {
10702
10702
  if (paths.length === 0) {
10703
10703
  return missingPluginLifecycleHealth(surface);
10704
10704
  }
10705
- let marker = null;
10706
- let newestMarkerMs = Number.NEGATIVE_INFINITY;
10705
+ const nowMs = (options.now ?? /* @__PURE__ */ new Date()).getTime();
10706
+ const genericPath = join3(healthDir, `${surface}.json`);
10707
+ const markerEntries = [];
10707
10708
  for (const path of paths) {
10708
10709
  try {
10709
10710
  const parsed = JSON.parse(
@@ -10712,15 +10713,20 @@ function localPluginLifecycleHealth(options) {
10712
10713
  if (!isRecord3(parsed) || parsed.surface !== surface) {
10713
10714
  continue;
10714
10715
  }
10715
- const candidateMs = typeof parsed.last_seen_at === "string" ? Date.parse(parsed.last_seen_at) : Number.NEGATIVE_INFINITY;
10716
- if (!marker || candidateMs > newestMarkerMs) {
10717
- marker = parsed;
10718
- newestMarkerMs = candidateMs;
10719
- }
10716
+ const components2 = isRecord3(parsed.components) ? parsed.components : {};
10717
+ markerEntries.push({
10718
+ path,
10719
+ marker: parsed,
10720
+ lastSeenMs: validLifecycleTimestamp(parsed.last_seen_at, nowMs),
10721
+ sessionStartMs: validLifecycleTimestamp(
10722
+ components2.session_start,
10723
+ nowMs
10724
+ )
10725
+ });
10720
10726
  } catch {
10721
10727
  }
10722
10728
  }
10723
- if (!marker) {
10729
+ if (markerEntries.length === 0) {
10724
10730
  return {
10725
10731
  ...missingPluginLifecycleHealth(surface),
10726
10732
  issues: [
@@ -10731,8 +10737,40 @@ function localPluginLifecycleHealth(options) {
10731
10737
  ]
10732
10738
  };
10733
10739
  }
10734
- const componentRecord = isRecord3(marker.components) ? marker.components : {};
10735
- const nowMs = (options.now ?? /* @__PURE__ */ new Date()).getTime();
10740
+ const genericEntry = markerEntries.find((entry) => entry.path === genericPath);
10741
+ const currentSessionStartMs = genericEntry?.sessionStartMs;
10742
+ const currentSessionEntries = typeof currentSessionStartMs === "number" ? markerEntries.filter(
10743
+ (entry) => entry.sessionStartMs >= currentSessionStartMs
10744
+ ) : [];
10745
+ const cohort = currentSessionEntries.length > 0 ? currentSessionEntries : [
10746
+ markerEntries.reduce(
10747
+ (latest, candidate) => candidate.lastSeenMs > latest.lastSeenMs ? candidate : latest
10748
+ )
10749
+ ];
10750
+ const newestEntry = cohort.reduce(
10751
+ (latest, candidate) => candidate.lastSeenMs > latest.lastSeenMs ? candidate : latest
10752
+ );
10753
+ const mergedComponents = {};
10754
+ for (const component of PLUGIN_HEALTH_COMPONENTS) {
10755
+ let latestValue;
10756
+ let latestMs = Number.NEGATIVE_INFINITY;
10757
+ for (const entry of cohort) {
10758
+ const components2 = isRecord3(entry.marker.components) ? entry.marker.components : {};
10759
+ const value = components2[component];
10760
+ const valueMs = validLifecycleTimestamp(value, nowMs);
10761
+ if (valueMs > latestMs) {
10762
+ latestMs = valueMs;
10763
+ latestValue = value;
10764
+ }
10765
+ }
10766
+ if (Number.isFinite(latestMs)) mergedComponents[component] = latestValue;
10767
+ }
10768
+ const marker = {
10769
+ ...newestEntry.marker,
10770
+ components: mergedComponents,
10771
+ last_seen_at: Number.isFinite(newestEntry.lastSeenMs) ? new Date(newestEntry.lastSeenMs).toISOString() : newestEntry.marker.last_seen_at
10772
+ };
10773
+ const componentRecord = mergedComponents;
10736
10774
  const sessionStartMs = validLifecycleTimestamp(
10737
10775
  componentRecord.session_start,
10738
10776
  nowMs
@@ -10840,7 +10878,7 @@ function localPluginLifecycleHealth(options) {
10840
10878
  hook_trust: hookTrust,
10841
10879
  components,
10842
10880
  issues,
10843
- explanation: "Each host session is tracked independently. SessionStart and the prompt hook must run first; tool and completion observations appear after those lifecycle events become eligible."
10881
+ explanation: "Each host process session is tracked independently. Turn-specific prompt, tool, and completion observations are combined only when they share the current SessionStart marker."
10844
10882
  };
10845
10883
  }
10846
10884
  function normalizedPluginHookTrust(value) {
@@ -10918,6 +10956,7 @@ function emptyPluginComponents() {
10918
10956
  }
10919
10957
  function lifecycleMarkerPaths(surface, healthDir, fileSystem) {
10920
10958
  const legacyPath = join3(healthDir, `${surface}.json`);
10959
+ const paths = fileSystem.exists(legacyPath) ? [legacyPath] : [];
10921
10960
  const listed = fileSystem.list ? (() => {
10922
10961
  try {
10923
10962
  return fileSystem.list(healthDir);
@@ -10927,9 +10966,9 @@ function lifecycleMarkerPaths(surface, healthDir, fileSystem) {
10927
10966
  })() : [];
10928
10967
  const sessionPaths = listed.filter(
10929
10968
  (name) => name.startsWith(`${surface}.`) && name.endsWith(".json") && /^[a-z_]+\.[a-f0-9]{24}\.json$/.test(name)
10930
- ).slice(0, MAX_LOCAL_LIFECYCLE_MARKERS).map((name) => join3(healthDir, name));
10931
- if (fileSystem.exists(legacyPath)) sessionPaths.push(legacyPath);
10932
- return [...new Set(sessionPaths)].slice(0, MAX_LOCAL_LIFECYCLE_MARKERS);
10969
+ ).slice(0, MAX_LOCAL_LIFECYCLE_MARKERS - paths.length).map((name) => join3(healthDir, name));
10970
+ paths.push(...sessionPaths);
10971
+ return [...new Set(paths)].slice(0, MAX_LOCAL_LIFECYCLE_MARKERS);
10933
10972
  }
10934
10973
  function normalizeConfig(value) {
10935
10974
  if (!isRecord3(value)) return {};
@@ -11083,7 +11122,7 @@ async function checkClientUpdate(input) {
11083
11122
  // src/server.ts
11084
11123
  var MAX_REMOTE_RESPONSE_BYTES = 4 * 1024 * 1024;
11085
11124
  var DOCTOR_PROBE_TIMEOUT_MS = 7500;
11086
- var SERVER_VERSION = true ? "0.1.63" : "0.0.0-dev";
11125
+ var SERVER_VERSION = true ? "0.1.66" : "0.0.0-dev";
11087
11126
  var tools = toolDefinitions;
11088
11127
  var doctorCliRequested = process.argv[2] === "doctor";
11089
11128
  var inputBuffer = Buffer.alloc(0);
@@ -11098,6 +11137,7 @@ var PRINCIPAL_SESSION_REQUIRED_TOOLS = /* @__PURE__ */ new Set([
11098
11137
  ]);
11099
11138
  var PRINCIPAL_SESSION_AWAITED_TOOLS = /* @__PURE__ */ new Set([
11100
11139
  ...PRINCIPAL_SESSION_REQUIRED_TOOLS,
11140
+ "get_effective_preferences",
11101
11141
  "report_task_outcome"
11102
11142
  ]);
11103
11143
  function apiAccessKey(access) {