Skip to content

feat(scheduler): durable jobs and Tencent safety#162

Merged
sweetcornna merged 1 commit into
mainfrom
feat/hermes-jobs-tencent-safety
Jul 22, 2026
Merged

feat(scheduler): durable jobs and Tencent safety#162
sweetcornna merged 1 commit into
mainfrom
feat/hermes-jobs-tencent-safety

Conversation

@sweetcornna

Copy link
Copy Markdown
Owner

Summary

Adds durable shadow/live scheduled-agent infrastructure with effect-safe Telegram/QZone delivery, and introduces default-on fail-closed Tencent freeze-risk protection across QQ, QQ Official, and QZone. Operator-specific migrated job implementations, schedules, targets, prompts, credentials, and continuity data remain outside the public repository.

Type

  • feat: new user-facing behavior
  • fix: bug fix or regression repair
  • refactor: internal change with no intended behavior difference
  • test: tests only
  • perf: performance optimization
  • docs: documentation only
  • chore: tooling, dependencies, or maintenance
  • ci: CI / workflow change

Scope / Packages Touched

  • corlinman-content-policy
  • corlinman-server scheduler, journal, gateway lifecycle/config, gRPC agent wiring
  • corlinman-agent OneBot/QZone dispatch
  • corlinman-channels QQ, QQ Official, Telegram delivery
  • ui QQ channel configuration

Behavior Proof

  • Added/updated automated tests.
  • Real production shadow runs exercised all eight private migrated jobs through the deployed scheduler/chat path; external effects and continuity writes remained zero. Six jobs completed in the final bounded pass; two research turns reached the harness timeout, while earlier persisted shadow records show at least one success for all eight.
  • Read-only deployed OneBot verification confirmed login info, friend list, and QZone cookies were readable after QR login.
  • proof: supplied

CI Gate

  • py-ruffuv run ruff check .
  • py-mypyuv run mypy python/packages/
  • py-test — 7276 passed, 7 skipped
  • ui-typecheck — passed
  • ui-lint — passed with existing warnings
  • ui-test — 1259 passed, 2 skipped
  • boundary-check — 2 contracts kept, 0 broken

Separate checks:

  • proto-sync — generated successfully, no drift
  • swift-mac — N/A
  • pnpm -C ui build — production export passed, route assertion OK

Module Boundaries / Proto

  • No new reverse imports across the Python layering contract.
  • Proto generation has no drift.

Risk & Rollback

Risk: scheduler/publication code and QQ safety boundaries affect automation and outbound transport. Runtime jobs imported for the migration remain shadow; agenda remains disabled. Effect receipts block ambiguous resends.

Rollback plan: pause affected Corlinman jobs, reconcile prepared/unknown/sent receipts, then revert this commit. Do not activate both Hermes and Corlinman for the same source occurrence. The private bundle and retained Hermes installation are unchanged by this PR.

Ownership

Cross-package change; self-reviewed under the current Claude self-review convention.

Codex Review

  • Ready for automatic Codex review (superseded by Claude self-review convention).
  • Claude pre-release review found and fixed unique OneBot WS echo IDs and Postgres aggregate parity.
  • Checked current bot/review state before merge.

Linked Issues

N/A

Checklist

  • PR title follows Conventional Commits.
  • Tests added/updated; behavior proof included.
  • No owner-area reviewer is required for this maintainer release.
  • No --no-verify or hook-skipping used.
  • Private Hermes job code/data and production secrets are absent from the commit.

Adds shadow/live runtime jobs, effect-safe delivery, private extension loading, scoped journal parity, and default-on QQ/QZone content-policy boundaries.
@github-actions github-actions Bot added codex:needs-review A Codex review is needed for the current PR head. codex:review-requested A Codex review was requested or should be running automatically. status: 🔁 re-review loop A fresh Codex review was requested after the latest change or comment. labels Jul 22, 2026
@sweetcornna
sweetcornna merged commit fa52464 into main Jul 22, 2026
9 checks passed
@sweetcornna
sweetcornna deleted the feat/hermes-jobs-tencent-safety branch July 22, 2026 01:45
@github-actions github-actions Bot added status: ✅ merge-ready Evidence and review are clear; normal merge gates may proceed. and removed codex:review-requested A Codex review was requested or should be running automatically. codex:needs-review A Codex review is needed for the current PR head. status: 🔁 re-review loop A fresh Codex review was requested after the latest change or comment. labels Jul 22, 2026

@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: af61e755d6

ℹ️ 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 365 to 366
if rj.enabled:
_register_runtime_loop(state, rj)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve catch-up when rehydrating runtime jobs

Rehydrated jobs are registered through SchedulerHandle.register, which starts _run_job_loop(..., catch_up=False). Consequently, when the service restarts shortly after an operator-created job's scheduled occurrence, that occurrence is silently skipped even if it is within CORLINMAN_SCHEDULER_CATCHUP_GRACE_SECS; only config-defined jobs receive startup catch-up. Boot rehydration needs a registration path that enables catch-up while ordinary create/edit registration remains catch-up-free.

Useful? React with 👍 / 👎.

Comment on lines 363 to 366
table[name] = rj
_sync_metadata(state, rj)
if rj.enabled:
_register_runtime_loop(state, rj)

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 Isolate registration failures per persisted row

If an enabled persisted row has an invalid cron/action or its registration raises, _register_runtime_loop propagates out of the entire rehydration loop. The gateway catches that only around the top-level boot hook, so all later valid rows remain unloaded and unregistered; moreover, _runtime_jobs has already set its loaded flag, preventing a retry. Catch registration errors for each row and remove or disable the rejected row before continuing.

Useful? React with 👍 / 👎.

Comment on lines +2734 to +2736
store = await SchedulerStore.open(
_resolve_data_dir() / "scheduler.sqlite"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Open the effect ledger in the gateway's resolved data directory

In a co-hosted deployment started with --data-dir or [server].data_dir while CORLINMAN_DATA_DIR is unset, the gateway opens and reconciles <resolved_data_dir>/scheduler.sqlite, but this servicer opens ~/.corlinman/scheduler.sqlite instead because _resolve_data_dir() only reads the environment. Scheduled QZone effects are therefore absent from the gateway-owned ledger and its reconciliation surface; in containers where the home directory is ephemeral, replacing the agent can also erase the reservation and permit the same public effect to be sent again. Pass the resolved data directory or the already-open scheduler store into the servicer.

Useful? React with 👍 / 👎.

Comment on lines 1622 to 1625
_sched_store = await SchedulerStore.open(
resolved_data_dir / "scheduler.sqlite"
resolved_data_dir / "scheduler.sqlite",
reconcile_prepared=True,
)

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 Avoid reconciling reservations owned by a live agent process

When the gateway and agent run as separate processes against the same SQLite file, restarting only the gateway executes this blanket reconciliation while the agent may still have an active prepared QZone send. The row is changed to unknown underneath that send, so a subsequent successful complete_effect(..., state="sent") conflicts and the job reports an unknown receipt even though the effect landed. Reconciliation needs ownership or lease information, or must run only when the process that owns reservations is known to have terminated.

Useful? React with 👍 / 👎.

Comment on lines +792 to +796
existing.execution_mode = (
"shadow" if body.execution_mode == "shadow" else "live"
)
existing.source_system = body.source_system
existing.source_job_id = body.source_job_id

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 imported jobs' immutable source identity

Posting an existing job name unconditionally replaces its source_system and source_job_id, including clearing them when a later upsert omits the fields. This contradicts the immutable source-identity contract and changes the prefix used for occurrence/effect keys; if an importer changes or omits the identity while recovering an occurrence whose effect landed but whose run record was lost, the ledger lookup uses a new key and can publish the effect again. Reject source-identity changes for an existing row rather than assigning these fields during an ordinary upsert.

Useful? React with 👍 / 👎.

Comment on lines +190 to +198
except Exception as exc:
# Telegram transport errors are ambiguous: the API may have accepted
# the request (or an earlier MSG_BREAK bubble) before the response was
# lost. Block replay until an operator reconciles the remote chat.
await _finish_effect(
store,
effect,
state="unknown",
error_code=type(exc).__name__,

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 Record local photo failures as definitive failures

When the photo path is missing, unreadable, or exceeds the local upload-size limit, TelegramSender.send_photo raises SendIoError before making any Telegram request, but this blanket handler records the reservation as unknown. That incorrectly places a definitely-unsent effect into the ambiguous-receipt state and requires remote reconciliation before the occurrence can be handled. Validate/read the local file before reservation, or catch SendIoError separately and complete the effect as failed.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added codex:reviewed Codex posted a review result or thumbs-up after the latest request. status: 👀 ready for maintainer look No automation blocker is known; a maintainer should inspect the current result. labels Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex:reviewed Codex posted a review result or thumbs-up after the latest request. status: ✅ merge-ready Evidence and review are clear; normal merge gates may proceed. status: 👀 ready for maintainer look No automation blocker is known; a maintainer should inspect the current result.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant