relay-dsh-plugin-events 0.1.0-internal.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yangbobo2021 and Relay contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Relay Events for DeepSeek Harness
2
+
3
+ `relay-dsh-plugin-events` is the durable, provider-neutral Wait/Event/Delivery
4
+ core for official DeepSeek Harness. It adds Agent tools, generic JSON ingress,
5
+ delivery to the owning existing Session, recovery, and a Waiting Events settings
6
+ surface without adding an execution backend.
7
+
8
+ ```bash
9
+ # After building this repository (see below):
10
+ dsh plugin --profile web add ./relay-dsh-plugin-events-0.1.0.tgz
11
+ dsh web
12
+ ```
13
+
14
+ Semantic routing and Monitor execution are separate plugins. Without a Router,
15
+ Events uses exact event-type matching. Without Monitors, Wait-only registration
16
+ and external ingress remain fully usable.
17
+
18
+ See [SPEC.md](SPEC.md) and
19
+ [delivery scenarios](docs/acceptance-scenarios.md).
20
+
21
+ Set `DSH_ROOT` to the prepared immutable official DSH checkout, then run
22
+ `npm ci --ignore-scripts && npm run verify && npm pack`.
23
+ The tarball includes built runtime files. A raw GitHub checkout intentionally does
24
+ not track `lib/`; do not install `#main` as if it were a built release. This delivery
25
+ does not claim an npm registry publication.
26
+
27
+ Tested official DSH reference:
28
+ `b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`.
package/SPEC.md ADDED
@@ -0,0 +1,82 @@
1
+ # Relay DSH Events Plugin Specification
2
+
3
+ Status: Accepted for `0.1.0`
4
+
5
+ ## Purpose
6
+
7
+ `relay-dsh-plugin-events` is the provider-neutral durable event core for official
8
+ DeepSeek Harness. It stores Waits and Events, selects a registered Router (or an
9
+ exact fallback), creates idempotent Deliveries, and admits each Delivery through
10
+ the owning existing DSH Session.
11
+
12
+ ## Boundary
13
+
14
+ The plugin owns:
15
+
16
+ - Wait, Event, routing-attempt, routing-decision, Delivery, Activation, and Monitor
17
+ persistence in one local SQLite database;
18
+ - the `relayEvents` Cordis service and its versioned Router/Monitor provider slots;
19
+ - exact event-type fallback routing when no Router provider is registered;
20
+ - delivery to the existing DSH Session, stable Activation identity, and background
21
+ recovery of queued Deliveries;
22
+ - `relay_register_waits` and `relay_cancel_waits` Agent tools;
23
+ - generic JSON ingress at `POST /api/relay/events`;
24
+ - the Waiting Events management Remote and Settings section.
25
+
26
+ The plugin does not own:
27
+
28
+ - model calls or semantic routing policy;
29
+ - Monitor observation, detection, scheduling, or generated code;
30
+ - email, CI, or provider-specific Webhook normalization;
31
+ - conversation creation or an execution backend;
32
+ - notification delivery for escalated Events.
33
+
34
+ ## Public Service Contract
35
+
36
+ `ctx.relayEvents.apiVersion` is `1`. The public service exposes high-level event
37
+ operations and two replaceable provider registrations:
38
+
39
+ - `registerRouter(provider)` accepts exactly one active Router with stable `id` and
40
+ `route({ event, eventRecord, sessions })`;
41
+ - `registerMonitorProvider(provider)` accepts exactly one active Monitor provider
42
+ with `prepare`, `checkMonitor`, and lifecycle-safe disposal;
43
+ - Wait/Event operations: `registerWaits`, `cancelWaits`, `listWaits`,
44
+ `handleEvent`, and `dispatchSession`;
45
+ - Monitor persistence operations: `beginMonitorCheck`, `completeMonitorCheck`,
46
+ `failMonitorCheck`, `abandonMonitorCheck`, and `listDueMonitors`. No SQLite
47
+ handle, mutable map, or DSH Agent object crosses the service boundary.
48
+
49
+ Provider registration is owned by the registering Cordis fiber. Duplicate active
50
+ providers fail closed. Unloading a provider restores the exact fallback Router or
51
+ the Monitor-unavailable state without unloading Events.
52
+
53
+ ## Reliability
54
+
55
+ - Event ingestion is idempotent by source identity and fingerprint.
56
+ - Routing decisions are validated against the current Wait snapshot and committed
57
+ once.
58
+ - Delivery creation and Wait claim are atomic.
59
+ - Failed admission keeps the same Event, Delivery, and Activation IDs queued.
60
+ - Admission succeeds when the stable inbox message has been flushed to DSH
61
+ persistence, not when the model turn finishes. A retry recognizes the persisted
62
+ Activation message and does not enqueue another follow-up.
63
+ - Reappearing Monitor trigger keys do not create another Delivery or pause a
64
+ rearmed Monitor; its replacement Wait stays active.
65
+ - A background recovery scan retries queued Deliveries after failure or Host restart.
66
+ - Events never create DSH Sessions and never switch the selected Web Session.
67
+ - Shutdown stops new operations, waits for in-flight work, disposes routes/listeners,
68
+ and closes SQLite last.
69
+
70
+ ## Security
71
+
72
+ - Loopback ingress may omit a token. Non-loopback ingress requires an exact Bearer
73
+ token and fails closed when no token is configured.
74
+ - Event fields remain untrusted content in the injected DSH envelope.
75
+ - Payload size is bounded before JSON parsing.
76
+ - Provider-specific signatures are outside this generic ingress boundary.
77
+
78
+ ## Delivery Acceptance
79
+
80
+ The release must pass unit, contract, packed-package, official-DSH, and composition
81
+ acceptance. The executable scenario list is in
82
+ [`docs/acceptance-scenarios.md`](docs/acceptance-scenarios.md).
@@ -0,0 +1,83 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ const BASE_DISPOSITIONS = new Set(["deliver", "escalate", "dismiss"]);
4
+
5
+ export function validateRoutingDecision({
6
+ decision,
7
+ sessions,
8
+ allowDeduplicate = false,
9
+ canDeduplicate = false,
10
+ label = "routing decision",
11
+ }) {
12
+ assert.ok(decision && typeof decision === "object", `${label}: decision is required`);
13
+ const dispositions = allowDeduplicate
14
+ ? new Set([...BASE_DISPOSITIONS, "deduplicate"])
15
+ : BASE_DISPOSITIONS;
16
+ assert.ok(
17
+ dispositions.has(decision.disposition),
18
+ `${label}: invalid disposition ${decision.disposition}`,
19
+ );
20
+ assert.ok(Array.isArray(decision.deliveries), `${label}: deliveries must be an array`);
21
+
22
+ if (decision.disposition === "deduplicate") {
23
+ assert.ok(canDeduplicate, `${label}: deduplicate requires an existing event`);
24
+ assert.equal(decision.deliveries.length, 0, `${label}: duplicate cannot create deliveries`);
25
+ return decision;
26
+ }
27
+
28
+ assert.equal(typeof decision.actionable, "boolean", `${label}: actionable must be boolean`);
29
+ assert.ok(Array.isArray(decision.evidence), `${label}: evidence must be an array`);
30
+ assert.equal(typeof decision.summary, "string", `${label}: summary must be a string`);
31
+
32
+ if (decision.disposition === "deliver") {
33
+ assert.equal(decision.actionable, true, `${label}: delivered event must be actionable`);
34
+ assert.ok(decision.deliveries.length > 0, `${label}: deliver requires a target`);
35
+ } else {
36
+ assert.equal(decision.deliveries.length, 0, `${label}: ${decision.disposition} cannot deliver`);
37
+ assert.equal(
38
+ decision.actionable,
39
+ decision.disposition === "escalate",
40
+ `${label}: actionable conflicts with ${decision.disposition}`,
41
+ );
42
+ }
43
+
44
+ const sessionsById = new Map(sessions.map((session) => [session.session_id, session]));
45
+ const deliveredSessionIds = decision.deliveries.map((delivery) => delivery.session_id);
46
+ assertUnique(deliveredSessionIds, `${label}: delivered session IDs`);
47
+
48
+ const selectedWaits = [];
49
+ for (const delivery of decision.deliveries) {
50
+ const session = sessionsById.get(delivery.session_id);
51
+ assert.ok(session, `${label}: unknown session ${delivery.session_id}`);
52
+ assert.ok(Array.isArray(delivery.wait_ids), `${label}: wait_ids must be an array`);
53
+ assertUnique(delivery.wait_ids, `${label}: delivered wait IDs`);
54
+ assert.equal(typeof delivery.relation, "string", `${label}: relation must be a string`);
55
+ assert.ok(
56
+ Number.isFinite(delivery.confidence) &&
57
+ delivery.confidence >= 0 &&
58
+ delivery.confidence <= 1,
59
+ `${label}: confidence must be between 0 and 1`,
60
+ );
61
+
62
+ for (const waitId of delivery.wait_ids) {
63
+ const wait = session.waits.find((candidate) => candidate.wait_id === waitId);
64
+ assert.ok(wait, `${label}: wait ${waitId} does not belong to ${session.session_id}`);
65
+ assert.equal(wait.status, "active", `${label}: wait ${waitId} is not active`);
66
+ selectedWaits.push(wait);
67
+ }
68
+ }
69
+
70
+ if (decision.deliveries.length > 1) {
71
+ assert.ok(selectedWaits.length > 0, `${label}: multi-session delivery needs matched waits`);
72
+ assert.ok(
73
+ selectedWaits.every((wait) => wait.exclusive === false),
74
+ `${label}: exclusive event cannot target multiple sessions`,
75
+ );
76
+ }
77
+
78
+ return decision;
79
+ }
80
+
81
+ function assertUnique(values, label) {
82
+ assert.equal(new Set(values).size, values.length, `${label} must be unique`);
83
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "required": ["disposition", "actionable", "deliveries", "evidence", "summary"],
6
+ "properties": {
7
+ "disposition": {
8
+ "enum": ["deliver", "escalate", "dismiss"]
9
+ },
10
+ "actionable": {
11
+ "type": "boolean"
12
+ },
13
+ "deliveries": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": ["session_id", "wait_ids", "relation", "confidence"],
19
+ "properties": {
20
+ "session_id": {
21
+ "type": "string"
22
+ },
23
+ "wait_ids": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "string"
27
+ }
28
+ },
29
+ "relation": {
30
+ "type": "string"
31
+ },
32
+ "confidence": {
33
+ "type": "number",
34
+ "minimum": 0,
35
+ "maximum": 1
36
+ }
37
+ }
38
+ }
39
+ },
40
+ "evidence": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "string"
44
+ }
45
+ },
46
+ "summary": {
47
+ "type": "string"
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,23 @@
1
+ export { validateRoutingDecision } from "./decision.mjs";
2
+
3
+ export const RELAY_EVENTS_API_VERSION = 1;
4
+
5
+ export function validateRouterProvider(provider) {
6
+ if (!provider || typeof provider !== "object") throw new TypeError("router provider is required");
7
+ if (!/^[a-z][a-z0-9._-]{0,63}$/u.test(provider.id ?? "")) {
8
+ throw new TypeError("router provider requires a lowercase stable id");
9
+ }
10
+ if (typeof provider.route !== "function") throw new TypeError("router provider requires route()");
11
+ return provider;
12
+ }
13
+
14
+ export function validateMonitorProvider(provider) {
15
+ if (!provider || typeof provider !== "object") throw new TypeError("monitor provider is required");
16
+ if (!/^[a-z][a-z0-9._-]{0,63}$/u.test(provider.id ?? "")) {
17
+ throw new TypeError("monitor provider requires a lowercase stable id");
18
+ }
19
+ for (const method of ["prepare", "checkMonitor"]) {
20
+ if (typeof provider[method] !== "function") throw new TypeError(`monitor provider requires ${method}()`);
21
+ }
22
+ return provider;
23
+ }
@@ -0,0 +1,5 @@
1
+ - insert:
2
+ - id: relay-events-host
3
+ name: 'relay-dsh-plugin-events'
4
+ config:
5
+ dispatchPollIntervalMs: 1000
@@ -0,0 +1,24 @@
1
+ # Events Delivery Acceptance Scenarios
2
+
3
+ Official DSH reference: `b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`
4
+
5
+ | ID | Scenario | Required result | Evidence |
6
+ | --- | --- | --- | --- |
7
+ | EVT-001 | Events-only boot | Packed plugin installs into a pristine official DSH profile and Host boots without Codex, Claude, Router, or Monitors. | package acceptance |
8
+ | EVT-002 | Exact fallback delivery | An Event whose type equals one active Wait is delivered once to that existing Session. | unit + Host contract |
9
+ | EVT-003 | Unmatched fallback | An unmatched Event is durably dismissed with an inspectable reason and starts no Session. | unit |
10
+ | EVT-004 | Duplicate ingress | Repeating the same provider identity/fingerprint returns the original Event and creates no second Delivery. | unit + HTTP |
11
+ | EVT-005 | Router substitution | A fake registered Router receives the compact snapshot; unload restores exact fallback. | service contract |
12
+ | EVT-006 | Duplicate Router | A second active Router registration fails without replacing the first. | service contract |
13
+ | EVT-007 | Monitor provider absence | Wait-only registration works; registration containing Monitors fails before replacing prior Waits. | service contract |
14
+ | EVT-008 | Atomic prepared Monitor registration | A fake Monitor provider baseline succeeds and Wait/Monitor records commit together; baseline failure changes neither. | integration |
15
+ | EVT-009 | Delivery retry identity | First admission fails, background recovery succeeds, and Event/Delivery/Activation IDs remain unchanged. | fake clock integration |
16
+ | EVT-010 | Restart recovery | A database with queued Delivery is reopened and recovered without re-routing or duplicate injection. | integration |
17
+ | EVT-011 | Ingress authorization | Loopback is accepted; non-loopback missing/wrong token is rejected; valid token succeeds. | HTTP unit |
18
+ | EVT-012 | Ingress bounds | Wrong method/content type, malformed JSON, and oversized bodies receive stable errors. | HTTP unit |
19
+ | EVT-013 | DSH tool ownership | Tools attach to every root Agent and use the authenticated Session ID rather than a model-supplied ID. | Host contract |
20
+ | EVT-014 | Management operations | List, open, cancel, and provider-backed run-now operate through the typed Remote. | Host/client contract |
21
+ | EVT-015 | Clean unload | New operations fail during shutdown; in-flight work settles; listeners, Remote, service, timers, and SQLite are released. | lifecycle integration |
22
+ | EVT-016 | Backend neutrality | Events-only, Events+Codex, and Events+Claude compositions contain no backend import or backend-name branch in Events. | static + official DSH |
23
+ | EVT-017 | Package boundary | Tarball contains built/public artifacts only and imports in a clean directory. | `npm pack` acceptance |
24
+
@@ -0,0 +1,68 @@
1
+ # Events Test Review
2
+
3
+ ## Review 1 — service extraction
4
+
5
+ - Preserved Runtime tests for replacement Waits, multi-Session delivery,
6
+ escalation, cancellation, ordering, and Activation identity.
7
+ - Added Router substitution/duplicate/disposal, Monitor baseline failure,
8
+ restart recovery, and shutdown drain coverage.
9
+ - The first run exposed an unhandled rejected Promise created by cleanup tracking;
10
+ handled success/error branches now remove in-flight Promises, and the regression
11
+ test passes without asynchronous activity warnings.
12
+
13
+ ## Review 2 — Agent bridge
14
+
15
+ - Verified tools derive Session ownership from authenticated Agent context.
16
+ - Verified Events owns only Wait registration/cancellation tools; timer ownership
17
+ moved to Monitors.
18
+ - Extended and asserted Monitor proposal fields without accepting a model-supplied
19
+ Session id.
20
+
21
+ Packed official-DSH boot/composition and browser checks run in Relay's cross-plugin
22
+ delivery harness.
23
+
24
+ ## Review 3 — independent checkout execution
25
+
26
+ - Verified a fresh plugin checkout can run `npm test` without relying on a parent
27
+ workspace installation.
28
+ - Added the same official-DSH peer-link preparation used by typecheck and build to
29
+ the test lifecycle; this prevents a false local pass caused by hoisted packages.
30
+
31
+ ## Review 4 — composed Router audit identity
32
+
33
+ - The cross-plugin test showed that a substituted Router delivered correctly while
34
+ its routing attempt was attributed to an internal slot name.
35
+ - Events now reads `name` and `model` dynamically from the active provider, and the
36
+ service test asserts provider attribution before fallback restoration.
37
+
38
+ ## Review 5 — submodule workspace type identity
39
+
40
+ - Running the plugin inside Relay exposed duplicate branded DSH types resolved from
41
+ the parent workspace instead of the pinned official checkout.
42
+ - The original wildcard peer-directory mapping did **not** fix package subpath
43
+ exports. The continuation replaces it with a generated map of official declaration
44
+ exports (including `/types` and transitive brands). Both typecheck and build now pass;
45
+ `dsh-type-paths.test.mjs` covers this resolution rule without suppressing errors.
46
+
47
+ ## Review 6 — real composition and admission lifecycle
48
+
49
+ - Persisted Observer identity in the Monitor manifest and restored it on hydration;
50
+ otherwise every non-clock observation failed after baseline despite passing fake-store tests.
51
+ - Grouped multiple matching Waits per Session in exact routing (one Delivery per Session).
52
+ - Agent tool disposers now belong to the plugin as well as the Agent, preventing stale tools after unload.
53
+ - Admission acknowledges inbox flush, not whole model-turn completion. Stable message IDs and
54
+ persisted inbox/message lookup prevent duplicate follow-ups after an acknowledgement retry.
55
+ - Added real SQLite composition regressions for recurring rearm, Observer failure, lease exclusion,
56
+ atomic baseline rollback and overdue restart; added background recovery and inbox retry unit tests.
57
+
58
+ ## Review 7 — packed delivery and composition
59
+
60
+ - Clean-directory tarball installation imports all public entries without parent
61
+ source or private runtime packages. Host-only Router/Monitors need no browser entry.
62
+ - Added real Cordis dependency appearance/removal/reappearance coverage; surviving
63
+ Agents retain no stale tools after plugin unload.
64
+ - Replaced misleading raw GitHub installation instructions with build-and-pack
65
+ instructions, since generated `lib/` is intentionally not tracked.
66
+ - A rearmed recurring Monitor now ignores already committed trigger keys without
67
+ pausing again. The regression checks disappearance/reappearance and the still-active
68
+ replacement Wait. Shutdown lease release is tested against real SQLite.