Skip to content

feat: 集成 Inspiration 与 Schedule 插件 - #36

Merged
CubePlus1 merged 39 commits into
mainfrom
codex/plugins-integration
Aug 26, 2026
Merged

feat: 集成 Inspiration 与 Schedule 插件#36
CubePlus1 merged 39 commits into
mainfrom
codex/plugins-integration

Conversation

@CubePlus1

Copy link
Copy Markdown
Owner

结果

一次性集成 EchoLog 的两条独立产品线及其共享通知基础:

  • Bundled Plugin API v1 新增受 notifications:send 权限保护的 notifications.send Core service;
  • Inspiration 插件覆盖独立灵感捕捉、Inbox/整理/归档、确定性 Flow、投递账本与日报;
  • Schedule 插件覆盖日程 CRUD、到点提醒、显式确认开始、snooze/complete/cancel,以及月/周/日 Web 视图;
  • Inspiration 与 Schedule 不互调 API、不共享表、不建立转换关系,只共同消费 Host 通知服务;
  • 合并两个插件的 build、registry、CLI、Web assets、文档与 lockfile,并增加组合验收。

关键语义

  • Schedule 到点只提醒,不会自动变成 active,也不会创建或启动 Core record;只有用户显式 confirm-start 才记录 confirmedStartAt。
  • Inspiration Flow 只支持查看、继续、保留、稍后和归档,不提供日程转换。
  • 插件失败保持 degraded 隔离,Core health 与主动记录链路不受影响。
  • ntfy 凭据继续由 Core 管理,插件只能取得发送函数和结构化逐渠道结果。

组合验证

  • pnpm test:170 passed,1 个既有 macOS helper 条件测试 skipped,0 failed;
  • pnpm typecheck:通过;
  • pnpm build:通过,包含 SDK、screen-time、tmux-status、Inspiration、Schedule 与 Core;
  • 隔离 PostgreSQL 数据库 pnpm test:integration:6/6 通过;
  • 实际 daemon smoke:Core health 200,Inspiration/Schedule 均 ready
  • Schedule/Inspiration API 创建与查询通过;
  • el schedule list --jsonel inspiration list --json 通过;
  • /plugins/schedule/index.js/plugins/inspiration/index.js 返回 200;
  • git diff --check:通过。

追踪

关联 #31#32#33#34#35。Trellis PRD、design、implement、check 与归档记录随对应组件一并纳入。

已知边界

外部通知传输无法提供严格 exactly-once:极窄的“外部已发送、ledger 尚未 finalize”崩溃窗口仍可能产生 at-least-once 行为;普通重复轮询、重启恢复和并发领取已由唯一键、reservation/claim 与测试覆盖。

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b751338a0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/inspiration/src/flow-store.ts Outdated
Comment on lines +581 to +584
if (delivery.status !== "sent") {
throw new FlowStoreError(
"INVALID_STATE",
`delivery ${deliveryId} is ${delivery.status}, not sent`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow outcomes for manually surfaced failed deliveries

When both notification channels are disabled or fail, FlowService.deliver still returns the manually surfaced candidate after changing its delivery to failed, and the Web UI renders the outcome buttons for that candidate. This guard rejects every one of those actions with 409, so users in notification-disabled or temporarily failing environments can see an inspiration but cannot mark it viewed, kept, later, or archived. Permit outcomes for a manually surfaced failed delivery, or avoid presenting it as actionable.

Useful? React with 👍 / 👎.

Comment on lines +62 to +67
return provider()(
{
title: "Inspiration",
message: candidate.inspiration.content,
},
signal

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass the delivery dedupe key to the notification transport

If the daemon exits or the Host times out after macOS/ntfy accepts this send but before finalizeNotification commits, the stale reserved row is intentionally recovered and sent again. Because this request carries only title and message, the downstream transport cannot recognize the retry's existing delivery dedupe key, so restart recovery can produce duplicate user notifications despite the durable ledger. The same stable idempotency key must reach the delivery boundary.

Useful? React with 👍 / 👎.

Comment on lines +521 to +523
SELECT * FROM inspiration_flow_deliveries
WHERE surfaced_at < ${before}
ORDER BY surfaced_at DESC, id 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.

P2 Badge Include the delivery id in the pagination boundary

When more than one delivery has the same surfaced_at and a page boundary splits that group, requesting the next page with the last row's timestamp uses a strict < predicate and skips every remaining row at that timestamp, even though ordering also uses id. This can occur with concurrent reservations or an injected/fixed clock; use a composite timestamp-and-id cursor matching the ordering.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d384adb328

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +112 to +116
const claimed = await store.claimReminder(
reminder.item.id,
reminder.reminderAt,
now
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop timed-out polls before claiming a reminder

When claimReminder is blocked on its FOR UPDATE longer than the Schedule job's 25-second timeout, the Host aborts the job and releases its non-reentry guard, but this call does not receive the abort signal. Once the lock clears, it can still insert the claim; the subsequent throwIfAborted() is caught and finalizes that claim as failed, so the exact reminder is permanently excluded without ever being sent. Pass the signal into the claim transaction and recheck it after acquiring the row lock and before inserting the ledger row.

Useful? React with 👍 / 👎.

@CubePlus1

Copy link
Copy Markdown
Owner Author

Schedule Abort/迟到写 P1 已在源 Worktree codex/schedule-plugin 本地修复,追加 commit c4bc6869f95914f48c829f5bcee817fafe68a375(按要求未 push,PR head 尚不会看到该 commit)。修复在 notifications.send settle 后及每个 terminal persistence 入口前复查 caller signal;signal.aborted/AbortError 在任何 finalize 前直接上抛,timeout/stop 后迟到 success/AbortError/普通 rejection 均保留 ledger=claimed,普通 channel/service failure 仍写 failed。新增真实 PluginHost timeout/stop/late-settlement 回归。验证:focused 23/23,Host suite 连续 5 次稳定,PostgreSQL 2/2,全仓 119 pass + 1 existing skip,typecheck/build/diff-check 通过;独立 reviewer 两轮 No findings(无 P0-P2)。Trellis 归档 commit:ea4119d

@CubePlus1

Copy link
Copy Markdown
Owner Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d384adb328

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +353 to +355
return {
id: "schedule",
async load() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh Schedule data during live polling

When a schedule changes outside this page's own actions—for example through el schedule, another API client, or simply by reaching its start time—the contribution never reloads it because it defines only load(), while the host's five-second loop invokes only loadLive(). The calendar and awaiting presentation therefore remain stale until a page reload or an unrelated full-book rebuild; provide a live loader and arrange a rebuild when its snapshot changes.

Useful? React with 👍 / 👎.

Comment thread plugins/inspiration/web/index.js Outdated
Comment on lines +75 to +79
latestInspirations = Array.isArray(list?.items) ? list.items : [];
latestDeliveries = Array.isArray(ledger?.deliveries) ? ledger.deliveries : [];
return {
inspirationList: list,
inspirationFlowDeliveries: ledger,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-render Inspiration faces after live updates

When a scheduled Flow run changes the ledger or another client edits an inspiration, this live loader updates latestInspirations and latestDeliveries, but the host's liveSignature() contains only Core record state and patchLiveDom() does not re-render plugin faces. Consequently the returned data is not displayed unless an unrelated Core change triggers a rebuild; signal the host when this snapshot changes or otherwise refresh the affected faces.

Useful? React with 👍 / 👎.

Comment thread plugins/schedule/src/reminders.ts Outdated
const item = reminder.item;
const description = item.description?.trim();
return [
`Scheduled for ${item.scheduledStartAt} (${item.timezone}).`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Render reminder times in the item's timezone

For items created with a non-UTC offset, scheduledStartAt is serialized from PostgreSQL as a UTC ISO string, so an item planned for 09:00+08:00 produces notification text such as 01:00:00Z (Asia/Shanghai). This makes the user-facing reminder appear eight hours off despite preserving the IANA display timezone; format the instant with item.timezone before constructing the message.

Useful? React with 👍 / 👎.

@CubePlus1

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 29cb75c1ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@CubePlus1

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e3dea89dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

latestDeliveryNextCursor = typeof page?.nextCursor === "string"
? page.nextCursor
: null;
return { handled: true, message: "已加载更多 Flow 投递" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve loaded Flow history across host refreshes

When a user clicks “加载更多投递”, this return value causes web/app.js to immediately call refreshBook(). That full refresh runs loadSnapshot(), which fetches only /flow/deliveries?limit=20 and replaces latestDeliveries, undoing the rows appended above before they can remain rendered; live polling would replace them again as well. Consequently, users cannot view beyond the first 20 deliveries, so the accumulated pages need to survive the refresh or the face must be rerendered without resetting to page one.

Useful? React with 👍 / 👎.

@CubePlus1

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: b8510021c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@CubePlus1

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: fe391aea4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@CubePlus1
CubePlus1 merged commit c7cc768 into main Aug 26, 2026
1 check passed
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