6.3 KiB
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
languageconfig is read throughself.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 scheduleadd_scheduled_event,get_scheduled_eventsanddelete_scheduled_eventread 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!scheand!dschecommands pass that key, anddelete_scheduled_eventrefuses 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
_eventsis keyed by the installation scope<instance_uuid>:<workspace_uuid>:<installation_uuid>and holds that installation's pending reminders only.runtime_revisionis deliberately excluded from the token, so a worker upgrade does not orphan a tenant's pending reminders within the process lifetime._loopsholds 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
languageconfig 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 atMAX_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 InstallationBindings
(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.