Files
LangBot/plugins/misc/ScheNotify/SHARED_RUNTIME.md

105 lines
6.3 KiB
Markdown

# Shared runtime placement (`shared-runtime-v1` + `stateless-v1`)
This source declares `execution.sharedRuntime: shared-runtime-v1` and
`execution.componentModel: stateless-v1` in `manifest.yaml`. It is a source
declaration only: it is **not certified**, has no signed archive, and has not been
accepted against a live multi-Workspace deployment. The exact archive still needs
independent review and acceptance runs.
Under shared placement one `ScheNotify` object and one of every declared component
serve every installation of the artifact digest. Installation attachments are
lightweight contexts; they do not create another object graph.
## Per invocation
- The `language` config is read through `self.plugin.get_config()` inside the
command and Tool components that use it. No config value is stored on the shared
object or in a scheduled event.
- `get_installation_binding()` is read per invocation and never retained past the
call; it decides which installation's schedule `add_scheduled_event`,
`get_scheduled_events` and `delete_scheduled_event` read and write.
- Inside one installation a reminder belongs to the full session identity
`(bot_uuid, target_type, target_id)` derived from the trusted platform session.
The `!sche` and `!dsche` commands pass that key, and `delete_scheduled_event`
refuses to remove a record that does not belong to the caller's session, so two
bots of one installation, or the person/group namespaces with the same id, are
never treated as the same session.
- `initialize()` is process-scoped and only logs: it runs once per worker with an
empty config, before any installation is known, so it starts no scheduler.
## Per binding, released on revocation
- `_events` is keyed by the installation scope
`<instance_uuid>:<workspace_uuid>:<installation_uuid>` and holds that
installation's pending reminders only. `runtime_revision` is deliberately excluded
from the token, so a worker upgrade does not orphan a tenant's pending reminders
within the process lifetime.
- `_loops` holds one minute-scheduler task per installation scope.
- `BasePlugin.on_installation_revoked(binding)` cancels that installation's
scheduler (bounded 5 s wait) and drops its pending reminders, so a revoked
installation can never fire an already-scheduled notification afterwards.
- The schedule is in-memory only (unchanged pre-existing behaviour): nothing about a
reminder is written to Host storage, so revocation has no durable row to clean up.
- Dedicated (binding-less) workers keep a single binding-less schedule and their
existing behaviour; their schedulers are stopped by process shutdown.
## The detached scheduler
Each installation's scheduler is a detached task, registered per installation and
stopped by revocation. It carries its installation scope as an explicit argument
and re-enters the invocation it was started from: the handler, config and binding
captured at scheduling time are passed back into `bind_invocation(...)` for the
whole life of the loop, so every `SEND_MESSAGE` a firing reminder makes is
attributable to the installation that scheduled it instead of falling back to the
worker's connection default. `on_installation_revoked(binding)` cancels the task,
which exits that scope and drops the authority with it; the same hook drops the
installation's pending reminders, so a revoked (or upgraded) installation can
neither fire nor send afterwards. Because the scope token excludes
`runtime_revision`, a worker upgrade presents a new binding for the same token;
the next scheduling invocation then cancels the superseded task and starts the
loop again under the new binding, while the installation's pending reminders are
kept. The scheduler never reads config or any other invocation-scoped API itself:
delivery is language-independent (`NOTIFY_PREFIX`).
## Known limits
- The scheduler is in-memory: pending reminders are lost on process restart (also
the pre-existing behaviour).
- Editing the `language` config only affects messages produced by later
invocations; notification delivery itself reads no config, because the prefix is
language-independent (the previous per-language lookup produced the same literal
for both supported languages and was removed).
- `get_scheduled_events()` returns a new list of the pending event records; the
records themselves are the live dicts, so callers must not mutate them.
- One installation's queue holds at most `MAX_EVENTS_PER_INSTALLATION` (100)
reminders and each stored message is capped at `MAX_MESSAGE_LENGTH` (2000)
characters; further reminders are rejected with a message instead of growing the
worker's memory. A delivery that the Host refuses is kept queued with its failure
count recorded, so it is retried on the next pass instead of being dropped.
- Per-installation state is only freed by revocation. The runtime revokes on
uninstall, disable, Workspace removal and upgrade.
- Revocation is serialized per installation but may overlap with sibling
installations' invocations; per-installation state is isolated, nothing is shared.
## Tests
`tests/test_installation_schedule.py` drives two distinct `InstallationBinding`s
(same instance and Workspace, different installation uuids) through one plugin
object with the installed SDK's `bind_invocation`, over a stub Host that records
notification relays. It asserts that B's `get_scheduled_events` returns only B's
reminder (pre-fix it returned both, from one shared list), that A's scheduler fires
only A's due reminder and leaves B's pending, that the scheduler is registered per
binding and stopped by revocation with the revoked installation's schedule dropped
(no late delivery) while a sibling's schedule survives, that `initialize()` starts
no scheduler, and that the scope token is the full binding.
The second-round tests add: the detached relay's Host call carries the scheduling
installation's binding (pre-fix `None`); a worker upgrade restarts the loop under
the new binding instead of delivering with the superseded task; `!sche`/`!dsche`
and the Tool use the full session identity, so another bot or namespace with the
same id is neither listed nor deleted (pre-fix the command listed and deleted the
other session's reminder); a refused send stays queued with its failure count; a
sibling delete during the awaited send no longer raises `ValueError` from
`list.remove`; and the queue is bounded while the reminder text is no longer
written to the debug log.