@remembrance-ai/opencode-plugin 0.2.1 → 0.2.3

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/src/index.mjs CHANGED
@@ -12,8 +12,10 @@
12
12
  // host's event surface allows:
13
13
  //
14
14
  // SESSION START (event -> session.created): records lifecycle health under the
15
- // "opencode" surface and logs the activation notice, so a user can see the
16
- // plugin loaded and knows to call run_connection_doctor.
15
+ // "opencode" surface, records a session created without a parent session
16
+ // as a main session and a child session's parent (the child belongs to its
17
+ // root session's tree), and logs the activation notice, so a user can see
18
+ // the plugin loaded and knows to call run_connection_doctor.
17
19
  //
18
20
  // PROMPT (chat.message -> experimental.chat.system.transform): queries from
19
21
  // the current user turn, then injects the bounded result into the system
@@ -49,12 +51,14 @@ import {
49
51
  acknowledgePrivateAuthorizationReminders,
50
52
  directSelectionFromResponse,
51
53
  highMatchFromResponse,
54
+ isRecordedMainSession,
52
55
  markHostPolicyAlertDelivered,
53
56
  observeSuccessfulCompletionTool,
54
57
  recordDirectiveFollowThroughForTool,
55
58
  recordDirectSelectionUse,
56
59
  recordDirectiveSurface,
57
60
  recordHighMatchSurface,
61
+ recordMainSession,
58
62
  recordPluginLifecycleHealth,
59
63
  recordHostPolicyDenial,
60
64
  recordQueryFeedbackObligation,
@@ -65,7 +69,9 @@ import {
65
69
  reportDirectiveEvent,
66
70
  reportTaskOutcomesOnStop,
67
71
  resolveApiCredential,
72
+ recordSessionParent,
68
73
  runPromptHook,
74
+ sessionTreeLedgerId,
69
75
  toolResponseIndicatesFailure,
70
76
  valueEpisodeFromResponse,
71
77
  writePromptedCount,
@@ -354,6 +360,21 @@ export const Remembrance = async (context = {}) => {
354
360
  try {
355
361
  const sessionId = sessionIdFromEvent(event, event);
356
362
  recordLifecycle("session_start", sessionId);
363
+ // OpenCode runs a task subagent in a child session, created with its
364
+ // parent's id (properties.info.parentID), and that session's
365
+ // chat.message reads like a user prompt. Only a session created with no
366
+ // parent is a main session. Any other one, a child or a session no
367
+ // process of this plugin saw created, gets no user decision about
368
+ // retained drafts; they wait, unacknowledged, for a main session. A
369
+ // child's recorded parent puts it in its root session's tree
370
+ // (sessionTreeLedgerId); a child whose link was never seen keeps its
371
+ // drafts as its own, and they reach the user once it has been idle.
372
+ const info = event?.properties?.info;
373
+ if (info?.id === sessionId && !info.parentID) {
374
+ recordMainSession(sessionId, env);
375
+ } else if (info?.id === sessionId) {
376
+ recordSessionParent(sessionId, info.parentID, env);
377
+ }
357
378
  const [, clientUpdate] = await Promise.all([
358
379
  (context.warmSession ?? warmPrincipalSession)(
359
380
  {
@@ -396,6 +417,7 @@ export const Remembrance = async (context = {}) => {
396
417
  messageSessions.set(messageKey, sessionId);
397
418
  turnContext.delete(sessionId);
398
419
  recordLifecycle("prompt_hook", sessionId);
420
+ const ledgerId = sessionTreeLedgerId(sessionId, env);
399
421
  const result = await (context.runPromptHook ?? runPromptHook)(prompt, {
400
422
  env,
401
423
  contextAdditions: pendingClientUpdates.has(sessionId)
@@ -410,13 +432,14 @@ export const Remembrance = async (context = {}) => {
410
432
  identity: { provider: SURFACE, model: "opencode" },
411
433
  reportDirectiveShown: false,
412
434
  deferReminderAcknowledgement: true,
413
- sessionId,
435
+ sessionId: ledgerId,
436
+ subagentSession: !isRecordedMainSession(sessionId, env),
414
437
  nativeSessionId,
415
438
  nativeContextBridge: context.nativeContextBridge,
416
439
  userAgent: "@remembrance-ai/opencode-plugin",
417
440
  });
418
441
  if (result?.eligible) {
419
- recordTaskEligibility(sessionId, env);
442
+ recordTaskEligibility(ledgerId, env);
420
443
  }
421
444
  const guidance = result?.context ?? "";
422
445
  if (guidance.trim()) {
@@ -431,6 +454,7 @@ export const Remembrance = async (context = {}) => {
431
454
  // injection observable as an absence of use rather than as silent
432
455
  // over-reporting.
433
456
  pendingGuidance.set(sessionId, {
457
+ ledgerId,
434
458
  directive: result?.directive ?? null,
435
459
  highMatch: result?.highMatch ?? null,
436
460
  text: guidance.trim(),
@@ -442,7 +466,7 @@ export const Remembrance = async (context = {}) => {
442
466
  } else if (result?.consumed) {
443
467
  // An empty task-shape search delivers nothing (owner decision 34C);
444
468
  // like any completed search it still ends an earlier directive.
445
- recordDirectiveSurface(sessionId, null, env);
469
+ recordDirectiveSurface(ledgerId, null, env);
446
470
  }
447
471
  }
448
472
 
@@ -485,7 +509,11 @@ export const Remembrance = async (context = {}) => {
485
509
  if (delivered.length > 0) turnContext.set(sessionId, delivered);
486
510
  output.system.push(...delivered);
487
511
  if (!guidance) return;
488
- acknowledgePrivateAuthorizationReminders(guidance.text, env);
512
+ acknowledgePrivateAuthorizationReminders(
513
+ guidance.text,
514
+ env,
515
+ guidance.ledgerId,
516
+ );
489
517
  pendingGuidance.delete(sessionId);
490
518
  if (guidance.deliveredContextAdditionIds.includes("client_update")) {
491
519
  pendingClientUpdates.delete(sessionId);
@@ -493,12 +521,20 @@ export const Remembrance = async (context = {}) => {
493
521
  // Delivery happened: only now is the guidance actually in the model's
494
522
  // context, so only now does it count as registry use.
495
523
  if (guidance.countsAsUse) {
496
- recordRegistryUse(sessionId, env);
497
- recordHighMatchSurface(sessionId, guidance.highMatch, env);
498
- recordValueEpisodeSurface(sessionId, guidance.valueEpisode, env);
524
+ recordRegistryUse(guidance.ledgerId, env);
525
+ recordHighMatchSurface(guidance.ledgerId, guidance.highMatch, env);
526
+ recordValueEpisodeSurface(
527
+ guidance.ledgerId,
528
+ guidance.valueEpisode,
529
+ env,
530
+ );
499
531
  }
500
- recordQueryFeedbackObligation(sessionId, guidance.queryFeedback, env);
501
- recordDirectiveSurface(sessionId, guidance.directive, env);
532
+ recordQueryFeedbackObligation(
533
+ guidance.ledgerId,
534
+ guidance.queryFeedback,
535
+ env,
536
+ );
537
+ recordDirectiveSurface(guidance.ledgerId, guidance.directive, env);
502
538
  if (guidance.directive) {
503
539
  await reportDirectiveEvent(
504
540
  {
@@ -577,16 +613,20 @@ export const Remembrance = async (context = {}) => {
577
613
  try {
578
614
  sessionId = sessionIdFromEvent(event, event);
579
615
  recordLifecycle("completion_hook", sessionId);
580
- await reportTaskOutcomesOnStop(sessionId, event, {
616
+ const ledgerId = sessionTreeLedgerId(sessionId, env);
617
+ await reportTaskOutcomesOnStop(ledgerId, event, {
581
618
  env,
582
619
  userAgent: "@remembrance-ai/opencode-plugin",
583
620
  });
584
- const decision = decideStop({ session_id: sessionId }, { env });
621
+ const decision = decideStop(
622
+ { session_id: ledgerId },
623
+ { env, subagentSession: !isRecordedMainSession(sessionId, env) },
624
+ );
585
625
  if (decision?.allow !== false || !decision?.reason) return;
586
626
  const delivered = await notify(client, decision.reason);
587
627
  if (!delivered) return;
588
- writePromptedCount(sessionId, decision.useCount ?? 1, env);
589
- acknowledgePrivateAuthorizationReminders(decision.reason, env);
628
+ writePromptedCount(ledgerId, decision.useCount ?? 1, env);
629
+ acknowledgePrivateAuthorizationReminders(decision.reason, env, ledgerId);
590
630
  } catch (error) {
591
631
  debugLog(env, "opencode_completion_hook_failed", {
592
632
  error: String(error),
@@ -659,33 +699,38 @@ export const Remembrance = async (context = {}) => {
659
699
  const normalizedTool = tool.toLowerCase();
660
700
  const toolArguments = toolArgumentsFromEvent(input);
661
701
  if (toolResponseIndicatesFailure(output)) {
702
+ // The call's own arguments let a denial park that exact draft.
662
703
  await observeHostPolicyDenial({
663
704
  eventType: "tool.execute.after",
664
705
  sessionId,
665
706
  toolName: tool,
666
- value: output,
707
+ value: {
708
+ ...(output && typeof output === "object" ? output : { output }),
709
+ tool_input: toolArguments,
710
+ },
667
711
  });
668
712
  return;
669
713
  }
670
714
  recordLifecycle("tool_observer", sessionId);
715
+ const ledgerId = sessionTreeLedgerId(sessionId, env);
671
716
  if (normalizedTool.endsWith("query_skills")) {
672
717
  if (queryResponseHasMatches(output)) {
673
- recordRegistryUse(sessionId, env);
718
+ recordRegistryUse(ledgerId, env);
674
719
  }
675
- recordHighMatchSurface(sessionId, highMatchFromResponse(output), env);
720
+ recordHighMatchSurface(ledgerId, highMatchFromResponse(output), env);
676
721
  recordValueEpisodeSurface(
677
- sessionId,
722
+ ledgerId,
678
723
  valueEpisodeFromResponse(output),
679
724
  env,
680
725
  );
681
726
  observeSuccessfulCompletionTool(
682
- sessionId,
727
+ ledgerId,
683
728
  tool,
684
729
  toolArguments,
685
730
  output,
686
731
  env,
687
732
  );
688
- await recordDirectiveFollowThroughForTool(sessionId, tool, output, {
733
+ await recordDirectiveFollowThroughForTool(ledgerId, tool, output, {
689
734
  env,
690
735
  });
691
736
  return;
@@ -693,19 +738,19 @@ export const Remembrance = async (context = {}) => {
693
738
  if (normalizedTool.endsWith("invoke_skill")) {
694
739
  const selection = directSelectionFromResponse(output);
695
740
  if (!selection) return;
696
- recordDirectSelectionUse(sessionId, selection, env);
741
+ recordDirectSelectionUse(ledgerId, selection, env);
697
742
  recordValueEpisodeSurface(
698
- sessionId,
743
+ ledgerId,
699
744
  valueEpisodeFromResponse(output),
700
745
  env,
701
746
  );
702
747
  clearHighMatchSurfaceForExplicitSelection(
703
- sessionId,
748
+ ledgerId,
704
749
  selection.slug,
705
750
  env,
706
751
  );
707
752
  observeSuccessfulCompletionTool(
708
- sessionId,
753
+ ledgerId,
709
754
  tool,
710
755
  toolArguments,
711
756
  output,
@@ -715,7 +760,7 @@ export const Remembrance = async (context = {}) => {
715
760
  }
716
761
  if (normalizedTool.endsWith("prepare_private_lesson_candidate")) {
717
762
  observeSuccessfulCompletionTool(
718
- sessionId,
763
+ ledgerId,
719
764
  tool,
720
765
  toolArguments,
721
766
  output,
@@ -742,7 +787,7 @@ export const Remembrance = async (context = {}) => {
742
787
  nativeSessionId: nativeSessionIdFromEvent(input, null),
743
788
  });
744
789
  observeSuccessfulCompletionTool(
745
- sessionId,
790
+ ledgerId,
746
791
  tool,
747
792
  toolArguments,
748
793
  output,
@@ -750,9 +795,9 @@ export const Remembrance = async (context = {}) => {
750
795
  );
751
796
  return;
752
797
  }
753
- clearHighMatchSurfaceIfOpened(sessionId, tool, toolArguments, env);
798
+ clearHighMatchSurfaceIfOpened(ledgerId, tool, toolArguments, env);
754
799
  observeSuccessfulCompletionTool(
755
- sessionId,
800
+ ledgerId,
756
801
  tool,
757
802
  toolArguments,
758
803
  output,