Skip to content

fix(cron): wire task-route lease into isolated cron delivery-target (closes #92460)#2

Closed
wangmiao0668000666 wants to merge 2386 commits into
mainfrom
fix/92460-task-route-lease
Closed

fix(cron): wire task-route lease into isolated cron delivery-target (closes #92460)#2
wangmiao0668000666 wants to merge 2386 commits into
mainfrom
fix/92460-task-route-lease

Conversation

@wangmiao0668000666

Copy link
Copy Markdown
Owner

Summary

Closes openclaw#92460 — isolated cron completion delivery drops explicit delivery.channel because the originator session entry is evicted, or the shared main session bucket was retargeted by another conversation, between job start and completion.

This PR introduces a generic task-route lease module keyed by detached run id, and wires the isolated cron completion path through it as a session-identity fallback.

Why

The detached-task completion delivery path (cron, subagent, ACP, codex) resolves its outbound origin at completion time. Today:

  • The originating session entry can be evicted from the session store before completion fires.
  • The shared agent-main session bucket is last-writer-wins across conversations, so the lastChannel/lastTo values can be a different conversation's room by the time the cron completes.
  • The explicit per-job delivery config in job.delivery may not survive all the way to the announce step because resolveDeliveryTarget resolves the target from session entry, not from job.delivery directly.

When the resolver falls through to the shared main bucket, the cron is either drained to the wrong room (cross-conversation leak) or refused by the openclaw#91613 inherited-room check. Either way the cron completion delivery is lost.

Solution

A SQLite-backed task-route lease row keyed by runId:

  • Acquired at task start (in createRunningTaskRun for the generic case, and explicitly in tryCreateCronTaskRun for cron because cron sets deliveryStatus: "not_applicable" which the auto-hook skips).
  • The requesterOrigin is a DeliveryContext capturing the cron job's own delivery config.
  • Read at completion time by resolveDeliveryTarget as a session-identity fallback, ranked storedDeliveryEntry ?? threadEntry ?? leaseEntry ?? mainEntry.
  • Settled on terminal run status (tryFinishCronTaskRun) so it can be GC'd instead of waiting for TTL expiry.
  • TTL-expired leases are GC'd by expireStaleTaskRouteLeases, available to gateway startup / a low-frequency timer.

The auto-settle hook in setDetachedTaskDeliveryStatusByRunId (added in this PR) retires the lease on any terminal TaskDeliveryStatus transition (delivered / session_queued → settled; failed → retired).

Changes

PR-A — lease module (commit 50711b7)

  • src/state/openclaw-state-schema.sql — new task_route_leases table (PRIMARY KEY run_id, with status/expires_at/indexes; no FK to task_runs because leases are created before the parent task_runs row in the cron flow)
  • src/state/openclaw-state-schema.generated.ts + src/state/openclaw-state-db.generated.d.ts — generated Kysely types
  • src/tasks/task-route-lease.ts — public API: acquireTaskRouteLease, getActiveTaskRouteLease, settleTaskRouteLease, mapDeliveryStatusToLeaseRetirement, extendTaskRouteLease, expireStaleTaskRouteLeases. Best-effort: never throws.
  • src/tasks/task-executor.ts — auto-acquire on createRunningTaskRun (skips when deliveryStatus: "not_applicable"); auto-settle on setDetachedTaskDeliveryStatusByRunId for terminal delivery statuses
  • src/tasks/task-route-lease.test.ts — 9 unit tests (acquire/getActive/settle/extend/expire/persist/re-acquire/map)
  • scripts/repro/issue-92460-task-route-lease-lifecycle.mts — 7-step real-DB repro

PR-B — isolated cron wiring (commit 55baaa7)

  • src/cron/isolated-agent/delivery-target.ts — read lease via getActiveTaskRouteLease(jobPayload.runId); precedence chain storedDeliveryEntry ?? threadEntry ?? leaseEntry ?? mainEntry; usedSharedMainFallback excludes leaseEntry so [Bug] Gateway reply-queue state not flushed on restart — stale queued replies drain to wrong Matrix room after restart openclaw/openclaw#91613 inherited-room refusal does not fire on lease origin
  • src/cron/isolated-agent/run.tsRunCronAgentTurnParams and resolveCronDeliveryContext accept runId; plumbed to resolveDeliveryTarget({ runId, ... })
  • src/cron/service/state.tsrunIsolatedAgentJob dep accepts runId
  • src/cron/service/timer.tsExecuteJobCoreOptions accepts runId; forwarded through executeJobCoreWithTimeoutexecuteJobCorestate.deps.runIsolatedAgentJob
  • src/cron/service/task-runs.tstryCreateCronTaskRun explicitly acquires the lease (auto-hook is skipped for not_applicable delivery); tryFinishCronTaskRun explicitly settles (success → settled, failure → retired); new helper resolveCronLeaseRequesterOrigin derives a DeliveryContext from job.delivery
  • src/gateway/server-cron.tsrunIsolatedAgentJob implementation forwards runId to runCronIsolatedAgentTurn
  • src/cron/isolated-agent/delivery-target.issue-92460.test.ts — 5 unit tests (lease origin wins over empty/stale main; lease takes lower precedence than stored context and thread session; [Bug] Gateway reply-queue state not flushed on restart — stale queued replies drain to wrong Matrix room after restart openclaw/openclaw#91613 interaction)
  • scripts/repro/issue-92460-cron-completion-leases.mts — 6-step real-DB repro

Real behavior proof

  • Behavior addressed: an isolated cron whose originating session entry was evicted (or whose shared main session bucket was retargeted by another conversation) before completion fires still delivers to the cron job's own delivery.channel / delivery.to via the task-route lease, instead of being drained to the wrong room or refused by the [Bug] Gateway reply-queue state not flushed on restart — stale queued replies drain to wrong Matrix room after restart openclaw/openclaw#91613 inherited-room check.

  • Real environment tested: Linux 4.19, Node 22 (per repo minimum), real on-disk SQLite via openOpenClawStateDatabase({ env: { OPENCLAW_STATE_DIR: stateDir } }) against a temp directory created with fs.mkdtempSync.

  • Exact steps or command run after this patch:

    • pnpm exec tsx scripts/repro/issue-92460-task-route-lease-lifecycle.mts — 7 PASS lines
    • pnpm exec tsx scripts/repro/issue-92460-cron-completion-leases.mts — 6 PASS lines
    • node scripts/run-vitest.mjs src/tasks/task-route-lease.test.ts — 9 tests passed
    • node scripts/run-vitest.mjs src/cron/isolated-agent/delivery-target.issue-92460.test.ts — 5 tests passed
    • node scripts/run-vitest.mjs src/cron/ src/tasks/ — 1566/1569 tests passed (the 3 failures are pre-existing flakes in detached-task-runtime.test.ts vi.spyOn(Date, "now") patterns, unrelated to this PR — same 33 failures appear on main without this PR)
    • pnpm tsgo:core — exit 0
    • pnpm build — exit 0
    • node scripts/run-oxlint.mjs <modified files> — exit 0
  • Evidence after fix (lease lifecycle repro, redacted temp dir path):

    === Reproduction for issue #92460 — task-route lease lifecycle ===
    State dir: /tmp/openclaw-92460-repro-XXXXXX
    PASS  1. acquire → getActive round-trip
    PASS  2. lease carries the captured requesterOrigin
    PASS  3. settle transitions the lease out of active
    PASS  4. extend on settled lease is a no-op
    PASS  5. expireStaleTaskRouteLeases GC (1 lease(s) expired)
    PASS  6. mapDeliveryStatusToLeaseRetirement maps terminal statuses
    PASS  7. lease persists across SQLite close + reopen
    ALL PASS  task-route lease lifecycle behaves as expected
    
  • Evidence after fix (cron completion repro):

    === Reproduction for issue #92460 — cron completion lease lifecycle ===
    State dir: /tmp/openclaw-92460-cron-repro-XXXXXX
    PASS  1. lease acquired at cron job start (runId=cron-morning-brief-1718726400000)
    PASS  2. resolver-side lookup reads the captured requesterOrigin
    PASS  3. terminal settle transitions the lease out of active
    PASS  4. failed-run retire transitions the lease out of active
    PASS  5. re-settle is idempotent
    PASS  6. lease persists across SQLite close + reopen
    ALL PASS  cron completion delivery is wired to the task-route lease
    
  • Observed result after fix: the lease is correctly acquired at cron job start, the resolver reads the captured requesterOrigin when session-key lookups miss, the lease is settled on terminal run status, and the row survives SQLite close+reopen (proves it lives in the shared state DB, not in-memory).

  • What was not tested: this PR does not exercise the live Telegram bot or webchat in-process channel. The end-to-end send path is gated by the resolver's ok: true return (proved by the unit tests), and the lease-only path is proven end-to-end against a real on-disk SQLite. A live channel run was not feasible in this environment.

Verification

  • pnpm tsgo:core → exit 0
  • pnpm build → exit 0
  • node scripts/run-oxlint.mjs <modified files> → exit 0
  • node scripts/run-vitest.mjs src/cron/ src/tasks/ → 1566 passed, 3 pre-existing flakes unrelated
  • 9 lease unit tests + 5 cron lease fallback unit tests = 14 new tests, all passing
  • 2 new repro scripts, all 13 steps PASS

AI assistance

AI-assisted (Claude). Real-environment repro scripts were run by the human on a clean checkout of the branch; unit tests, typecheck, lint, and build all pass on Linux Node 22. Code is read-and-understood: the design was a joint effort with @vincentkoc's openclaw#92580 close comment as the design constraint (R1-R4), the cron side is wired to mirror the existing tryCreateCronTaskRun / tryFinishCronTaskRun pattern, and the deliveryStatus: "not_applicable" skip in the auto-hook is the reason cron needs the explicit acquire/settle.

Related

vincentkoc and others added 30 commits June 19, 2026 03:19
* fix(agents): skip fallback auth gate for CLI runtimes

* fix(agents): skip auth gate for CLI-owned transport

* fix(agents): skip auth gate for CLI-owned transport

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
…enclaw#88581)

* Add /name chat command to rename the current session

Adds a `/name <title>` slash command so users can name or rename the
current session directly from any chat channel, instead of only through
the web/admin session manager. This keeps parallel sessions easy to tell
apart from within the chat flow.

Behaviour:
- `/name <title>` sets the session label, reusing the canonical
  `parseSessionLabel` validation (trim, non-empty, max 512 chars) and the
  same cross-store uniqueness rule enforced by the web `sessions.patch`
  path, so chat naming behaves identically to the session manager.
- `/name` with no argument shows the current name plus a locally derived
  `deriveSessionTitle` suggestion without mutating anything (no LLM).
- Only authorized senders can rename (rejectUnauthorizedCommand), matching
  /goal. The label surfaces everywhere sessions.list is shown (TUI, web,
  CLI, MCP).

The handler resolves the session via resolveSessionStoreEntry so renames
land on the canonical entry even when the store still holds a legacy or
case-folded key alias, and excludes those aliases from the uniqueness scan
to avoid false conflicts. Failed renames skip the store write.

Registers the command in commands-registry.shared.ts and the handler in
loadCommandHandlers, documents it in docs/tools/slash-commands.md, and adds
unit tests covering rename, no-arg suggestion, duplicate-label rejection,
unauthorized senders, disabled text commands, and persisted-name re-read.

Part of the chat-native session naming feature (follows the web in-chat
rename PR). Relates to openclaw#85502 and openclaw#54397.

Co-authored-by: Copilot <[email protected]>

* fix(name): seed native sessions and persist renames via canonical key

Address Codex review on PR openclaw#88581:
- Fall back to the in-memory params.sessionEntry when the store has no row
  yet, so a brand-new native slash session can be named from its first
  /name command instead of failing with 'no active session to name'.
- Persist the rename through resolved.normalizedKey and drop legacy/
  case-folded alias keys (mirroring persistResolvedSessionEntry) so the
  canonical entry is updated and sessions.list stops surfacing the stale
  alias row.

Co-authored-by: Copilot <[email protected]>

* fix(name): emit session metadata changes

Route successful /name renames through the shared command session metadata seam so subscribed session lists receive sessions.changed like /goal.

Co-authored-by: Copilot <[email protected]>

* feat(commands): add /name to rename the current session from chat

* fix(docs): document the /name slash command

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Agent <[email protected]>
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Dependency Guard

This PR changes dependency-related files. Maintainers should confirm these changes are intentional.

Changed files:

  • extensions/acpx/npm-shrinkwrap.json
  • extensions/acpx/package.json
  • extensions/admin-http-rpc/package.json
  • extensions/alibaba/package.json
  • extensions/amazon-bedrock-mantle/npm-shrinkwrap.json
  • extensions/amazon-bedrock-mantle/package.json
  • extensions/amazon-bedrock/npm-shrinkwrap.json
  • extensions/amazon-bedrock/package.json
  • extensions/anthropic-vertex/npm-shrinkwrap.json
  • extensions/anthropic-vertex/package.json
  • extensions/anthropic/package.json
  • extensions/arcee/npm-shrinkwrap.json
  • extensions/arcee/package.json
  • extensions/azure-speech/package.json
  • extensions/bonjour/package.json
  • extensions/brave/npm-shrinkwrap.json
  • extensions/brave/package.json
  • extensions/browser/package.json
  • extensions/byteplus/package.json
  • extensions/canvas/package.json
  • extensions/cerebras/npm-shrinkwrap.json
  • extensions/cerebras/package.json
  • extensions/chutes/npm-shrinkwrap.json
  • extensions/chutes/package.json
  • extensions/clickclack/package.json
  • 166 additional dependency-related files not shown

Maintainer follow-up:

  • Review whether the dependency changes are intentional.
  • Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.
  • Treat package-lock.json and npm-shrinkwrap.json diffs as security-review surfaces.
  • Run pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json locally for detailed release-style evidence.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Dependency graph changes noted

This PR includes dependency graph changes. The dependency guard is informational because the PR author is a repository admin or a member of @openclaw/openclaw-secops.

  • Current SHA: 353681104591bc7ba962adcba9244f74737cf4cb
  • Trusted actor: @wangmiao0668000666
  • Trusted role: pull request author; repository admin

Security review is still recommended before merge when the dependency graph change is intentional.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Isolated cron completion announcer drops explicit delivery.channel on final controller return