diff --git a/.hop/records/prompts/P_06FN83DN7R0B9X5TEV3YDC0.json b/.hop/records/prompts/P_06FN83DN7R0B9X5TEV3YDC0.json new file mode 100644 index 0000000..84dee3f --- /dev/null +++ b/.hop/records/prompts/P_06FN83DN7R0B9X5TEV3YDC0.json @@ -0,0 +1,17 @@ +{ + "id": "P_06FN83DN7R0B9X5TEV3YDC0", + "task_id": "T_06FN83DN7SMZB9WJRZNZMTR", + "attempt_id": "AT_06FN83DN7VHVF5BVW6DEJ8R", + "state_id": "P_06FN83DN7R0B9X5TEV3YDC0", + "prompt": "# Files mentioned by the user:\n\n## Screenshot 2026-07-11 at 6.58.29 PM.png: /var/folders/58/b6yn15tx3jb7_6lw9m67f89m0000gn/T/TemporaryItems/NSIRD_screencaptureui_vyA0oe/Screenshot 2026-07-11 at 6.58.29 PM.png\n\n## My request for Codex:\nonce again, there are prompts here that arent mine!", + "agent_name": "codex", + "status": "proposed", + "response_summary": "Purge internal reconciliation prompts from portable records", + "created_at": "2026-07-12T02:00:12.350322Z", + "metadata": { + "source_tree": "27e94b6d19eb7e4107b127e3cec0ca41718053dc", + "git_commit": "74ba06de917236754f6146aca0e2d6e10438e455", + "attempt_head": "C_06FN83MVSDWQH772HR663SR", + "attempt_head_kind": "checkpoint" + } +} diff --git a/internal/hop/ledger.go b/internal/hop/ledger.go index 7f08b3f..e28cd5d 100644 --- a/internal/hop/ledger.go +++ b/internal/hop/ledger.go @@ -81,6 +81,7 @@ func (s *Service) exportPromptLedger(ctx context.Context, destinationRoot string wantedAttempts[attemptID] = struct{}{} } lastPromptByAttempt := make(map[string]string) + excludedPromptIDs := make(map[string]struct{}) for _, row := range graph { if row.State.Kind == StatePrompt && row.State.Prompt != "" && row.State.AttemptID != "" { lastPromptByAttempt[row.State.AttemptID] = row.State.ID @@ -92,9 +93,11 @@ func (s *Service) exportPromptLedger(ctx context.Context, destinationRoot string continue } if _, hidden := suppressed[state.ID]; hidden { + excludedPromptIDs[state.ID] = struct{}{} continue } if _, reconciliation := decodeReconciliationConflicts(state.Summary); reconciliation || strings.HasPrefix(state.Prompt, "Resolve proposal ") { + excludedPromptIDs[state.ID] = struct{}{} continue } if len(wantedAttempts) > 0 { @@ -156,6 +159,12 @@ func (s *Service) exportPromptLedger(ctx context.Context, destinationRoot string if err := os.MkdirAll(outputDir, 0o755); err != nil { return PortablePromptLedger{}, fmt.Errorf("create prompt ledger directory: %w", err) } + for promptID := range excludedPromptIDs { + outputPath := filepath.Join(outputDir, promptID+".json") + if err := os.Remove(outputPath); err != nil && !os.IsNotExist(err) { + return PortablePromptLedger{}, fmt.Errorf("remove excluded prompt record %s: %w", promptID, err) + } + } for _, record := range ledger.Prompts { outputPath := filepath.Join(outputDir, record.ID+".json") _, statErr := os.Stat(outputPath) diff --git a/internal/hop/service_smoke_test.go b/internal/hop/service_smoke_test.go index 240b98e..eac7244 100644 --- a/internal/hop/service_smoke_test.go +++ b/internal/hop/service_smoke_test.go @@ -279,6 +279,29 @@ func TestExportOmitsActiveInternalAttempt(t *testing.T) { } } +func TestExportRemovesPreviouslyPublishedReconciliationPrompt(t *testing.T) { + ctx := context.Background() + service, _ := newTestProject(t, map[string]string{"base.txt": "base\n"}) + result, err := service.CreatePrompt(ctx, "Resolve proposal R_TEST against accepted state A_TEST", "", "codex") + if err != nil { + t.Fatal(err) + } + recordDir := filepath.Join(service.Root, ".hop", "records", "prompts") + if err := os.MkdirAll(recordDir, 0o755); err != nil { + t.Fatal(err) + } + recordPath := filepath.Join(recordDir, result.Prompt.ID+".json") + if err := os.WriteFile(recordPath, []byte("{}\n"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := service.ExportPromptLedger(ctx, service.Root); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(recordPath); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("internal reconciliation record still exists: %v", err) + } +} + func TestProposeRespectsSuppressedPromptManifest(t *testing.T) { ctx := context.Background() service, _ := newTestProject(t, map[string]string{"base.txt": "base\n"})