Skip to content

【substack-mcp】Notes投稿履歴をD1へ保存して重複を防止する - #11

Draft
nanameru wants to merge 9 commits into
issue-8-note-errorfrom
issue-10-note-history
Draft

nanameru wants to merge 9 commits into
issue-8-note-errorfrom
issue-10-note-history

Conversation

@nanameru

@nanameru nanameru commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Closes #10

変更概要

  • Cloudflare D1へNotes投稿履歴を保存
  • 正規化本文のSHA-256と一意制約で完全一致の二重投稿を防止
  • 上流結果が不明な場合はunknownとして保存し、同一本文を自動再投稿しない
  • 読み取り専用のlist_note_historyツールを追加
  • D1 migration、Node回帰テスト、README、test-boardを追加

検証結果

  • npm test: 4件成功
  • npm run type-check: 成功
  • Python回帰テスト: 7件成功
  • Python構文検査: 成功
  • D1 migrationのローカル適用とschema確認: 成功
  • wrangler deploy --dry-run: 成功

リスク

  • 意味的に似た内容の判定はChatGPTが履歴を参照して行い、D1の強制拒否は正規化本文の完全一致に限定
  • 本番D1 migrationとWorkerデプロイは完了
  • GPT Workプロンプトの正本は追加済みだが、ChatGPT上の定期タスク保存は操作接続の復旧待ち
  • 本PRはIssue #8の修正ブランチをbaseにしたstacked PR

Cookie接続診断(2026-09-16)

  • CSRF_COOKIE_MISSINGの切り分け用に、/auth/diagnosticsを追加。認証に使わない2分間のCookieで通常名と__Host-名のGET/POST到達を比較します。
  • 結果は固定ラベルと真偽値のみ。OAuth/CSRF検証、KV/D1、Substack操作は変更しません。
  • TC-006追加。Nodeテスト12件、型検査、git diff --check、既存Containerを保持するWrangler dry-runに成功。ローカルChromeでもGET/POSTの両Cookie到達を確認しました。
  • NodeからTypeScriptモジュールを読むため明示的.ts importとallowImportingTsExtensionsを使用。初回のimport解決エラーと型検査エラーを修正済み。
  • Commit: 3e591e3。診断ページは本番反映の承認待ちで、OAuth接続の問題はまだ未解決です。

OAuth戻り先の遷移修正

診断ページは本番反映済み。ChromeでGET/POST両Cookieの到達を確認しました。一方、最初の認証POSTとGitHub callbackは302成功でも画面が戻らず、再送信時に消費済みCSRF Cookieが欠落しました。フォームのCSPに登録済みcallback originを含め、ChatGPTへ戻る遷移を許可します。URLのパス・クエリはCSPへ含めません。

  • Commit: 02fe53c
  • TC-007、14件のNodeテスト、型検査、dry-run成功
  • 認証ページ修正の本番反映と接続E2Eは確認待ち

本番接続復旧の確認

2026-09-16、Worker 6da579c2-8182-421b-b8cb-5accc0d7d0edへCSP修正を反映。ChromeでChatGPTへの戻り・接続完了・12個のActionsを確認しました。TC-007の証跡はa540e05。下書き・履歴の実取得と定期タスクの更新/再開は未実施です。

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR implements Substack Notes publication history tracking in D1 to prevent duplicate posts. The implementation is well-structured with comprehensive test coverage. However, there are critical issues that must be fixed before merge:

Critical Issues (Must Fix)

  1. Database update verification missing: markNotePublished and markNoteUnknown do not verify that database updates succeeded, creating potential for inconsistent state
  2. Performance regression: Missing index on content_hash column causes full table scans on duplicate checks, degrading performance with table growth

Positive Aspects

  • Well-designed duplicate detection using content normalization and SHA-256 hashing
  • Comprehensive test coverage with FakeD1 implementation
  • Proper error handling with custom DuplicateNoteError
  • Clean separation of concerns with note-history module

Please address the critical issues identified in the inline comments.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Comment thread worker/note-history.ts
Comment on lines +109 to +122
export async function markNoteUnknown(
db: D1Database,
reservation: NoteReservation,
now = new Date(),
): Promise<void> {
await db
.prepare(
`UPDATE note_publications
SET status = 'unknown', updated_at = ?
WHERE attempt_id = ? AND status = 'pending'`,
)
.bind(now.toISOString(), reservation.attemptId)
.run();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: markNoteUnknown does not verify that database update succeeded. If the WHERE clause matches no rows, the function silently succeeds without indication of failure. This creates inconsistent state where the application believes status was updated to 'unknown' but the database remains unchanged.

Suggested change
export async function markNoteUnknown(
db: D1Database,
reservation: NoteReservation,
now = new Date(),
): Promise<void> {
await db
.prepare(
`UPDATE note_publications
SET status = 'unknown', updated_at = ?
WHERE attempt_id = ? AND status = 'pending'`,
)
.bind(now.toISOString(), reservation.attemptId)
.run();
}
export async function markNoteUnknown(
db: D1Database,
reservation: NoteReservation,
now = new Date(),
): Promise<void> {
const result = await db
.prepare(
`UPDATE note_publications
SET status = 'unknown', updated_at = ?
WHERE attempt_id = ? AND status = 'pending'`,
)
.bind(now.toISOString(), reservation.attemptId)
.run();
if ((result.meta.changes ?? 0) === 0) {
throw new Error(`Failed to mark note as unknown: attempt_id ${reservation.attemptId} not found or not in pending status`);
}
}

Comment thread worker/note-history.ts
Comment on lines +91 to +107
export async function markNotePublished(
db: D1Database,
reservation: NoteReservation,
noteId: string | null,
noteUrl: string | null,
now = new Date(),
): Promise<void> {
const publishedAt = now.toISOString();
await db
.prepare(
`UPDATE note_publications
SET status = 'published', note_id = ?, note_url = ?, published_at = ?, updated_at = ?
WHERE attempt_id = ?`,
)
.bind(noteId, noteUrl, publishedAt, publishedAt, reservation.attemptId)
.run();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: markNotePublished and markNoteUnknown do not verify that database updates succeeded. If the WHERE clause matches no rows (e.g., attemptId doesn't exist or was already updated), the functions silently succeed without indication. This creates inconsistent state where the application believes the operation succeeded but the database was not modified.

Suggested change
export async function markNotePublished(
db: D1Database,
reservation: NoteReservation,
noteId: string | null,
noteUrl: string | null,
now = new Date(),
): Promise<void> {
const publishedAt = now.toISOString();
await db
.prepare(
`UPDATE note_publications
SET status = 'published', note_id = ?, note_url = ?, published_at = ?, updated_at = ?
WHERE attempt_id = ?`,
)
.bind(noteId, noteUrl, publishedAt, publishedAt, reservation.attemptId)
.run();
}
export async function markNotePublished(
db: D1Database,
reservation: NoteReservation,
noteId: string | null,
noteUrl: string | null,
now = new Date(),
): Promise<void> {
const publishedAt = now.toISOString();
const result = await db
.prepare(
`UPDATE note_publications
SET status = 'published', note_id = ?, note_url = ?, published_at = ?, updated_at = ?
WHERE attempt_id = ?`,
)
.bind(noteId, noteUrl, publishedAt, publishedAt, reservation.attemptId)
.run();
if ((result.meta.changes ?? 0) === 0) {
throw new Error(`Failed to mark note as published: attempt_id ${reservation.attemptId} not found`);
}
}

Comment on lines +13 to +14
CREATE INDEX IF NOT EXISTS idx_note_publications_attempted_at
ON note_publications(attempted_at DESC);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Performance Regression: Missing index on content_hash column causes full table scan on every duplicate check. The reserveNote function queries WHERE content_hash = ? without an index, resulting in O(n) lookup time instead of O(log n). This degrades performance proportionally with table size. Add index for the content_hash column.

Suggested change
CREATE INDEX IF NOT EXISTS idx_note_publications_attempted_at
ON note_publications(attempted_at DESC);
CREATE INDEX IF NOT EXISTS idx_note_publications_content_hash
ON note_publications(content_hash);
CREATE INDEX IF NOT EXISTS idx_note_publications_attempted_at
ON note_publications(attempted_at DESC);

@nanameru

nanameru commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

GPT Work定期実行の文体・禁止話題・重複回避・失敗時停止ルールを docs/gpt-work-note-schedule-prompt.md に追加しました(commit: f667cd2)。D1本番migrationとWorkerデプロイも完了し、公開済み直近3件を初期履歴として登録済みです。

@nanameru

Copy link
Copy Markdown
Owner Author

訂正: GPT Work定期実行の文体・禁止話題・重複回避・失敗時停止ルールを docs/gpt-work-note-schedule-prompt.md に追加したcommitは f667cd2 です。D1本番migrationとWorkerデプロイも完了し、公開済み直近3件を初期履歴として登録済みです。

@nanameru

Copy link
Copy Markdown
Owner Author

OAuth接続の固定名CSRF Cookie衝突をc11cb5aで修正し、本番Worker 7259148c-3362-42a8-9ffa-f4312cfb044dへ反映しました。Node 5/5、Python 7/7、type-check、dry-runに成功。本番で並行2認証フローが両方GitHubへ302遷移することを確認しています。残りはユーザー承認後のlist_note_history読み取り確認です。

@nanameru

Copy link
Copy Markdown
Owner Author

CSRF再発の診断・キャッシュ防止を本番反映

  • コミット: 38a22a6 (push済み)
  • 本番Worker: 8ea5c238-38ee-4d3c-811c-e14e570ff45b
  • TC-005: Cookie未送信、フォーム欠落、値不一致を固定コードで区別。秘密の値をレスポンスへ出さず、CSRF拒否を維持する。
  • ブラウザには日本語の復旧案内、APIにはJSON診断。認証ページ・エラー・リダイレクトをno-storeに変更。
  • npm test: 9件成功。型検査・git diff --check成功。
  • 通常dry-runはDocker CLI未起動で失敗。OAuthのみの変更のため、Wranglerが案内する--containers-rollout=noneで既存コンテナを保持し、dry-runと本番デプロイ成功。
  • 本番未認証POSTでJSON/HTMLとも400、CSRF_COOKIE_MISSING、no-storeを確認。OAuthクライアント作成、GitHub認証、Substack操作は行っていない。
  • Beads: substack-mcp-issue-6-cloudflare-deploy-jub.8

実ブラウザのCookie送信と接続成功は未確認。再発原因はまだ特定できておらず、根本解決とは扱わない。次回Connectで新しいエラーコードを確認し、Cookie未送信と値不一致を切り分ける。定期プロンプト更新と履歴MCP接続確認も未完了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant