@dsh-enhanced/assistant-work-items 0.0.0-stage → 0.1.66
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 +21 -0
- package/README.md +685 -2
- package/cordis.patch.yml +11 -0
- package/lib/acceptance-binding.d.ts +86 -0
- package/lib/acceptance-binding.d.ts.map +1 -0
- package/lib/acceptance-binding.js +250 -0
- package/lib/acceptance-binding.js.map +1 -0
- package/lib/admission.d.ts +95 -0
- package/lib/admission.d.ts.map +1 -0
- package/lib/admission.js +642 -0
- package/lib/admission.js.map +1 -0
- package/lib/authority.d.ts +92 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +205 -0
- package/lib/authority.js.map +1 -0
- package/lib/config.d.ts +365 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +672 -0
- package/lib/config.js.map +1 -0
- package/lib/continuation-contract.d.ts +126 -0
- package/lib/continuation-contract.d.ts.map +1 -0
- package/lib/continuation-contract.js +109 -0
- package/lib/continuation-contract.js.map +1 -0
- package/lib/continuation-record.d.ts +32 -0
- package/lib/continuation-record.d.ts.map +1 -0
- package/lib/continuation-record.js +57 -0
- package/lib/continuation-record.js.map +1 -0
- package/lib/continuation.d.ts +89 -0
- package/lib/continuation.d.ts.map +1 -0
- package/lib/continuation.js +607 -0
- package/lib/continuation.js.map +1 -0
- package/lib/diagnosis-contract.d.ts +95 -0
- package/lib/diagnosis-contract.d.ts.map +1 -0
- package/lib/diagnosis-contract.js +167 -0
- package/lib/diagnosis-contract.js.map +1 -0
- package/lib/diagnosis.d.ts +70 -0
- package/lib/diagnosis.d.ts.map +1 -0
- package/lib/diagnosis.js +466 -0
- package/lib/diagnosis.js.map +1 -0
- package/lib/dispatch.d.ts +100 -0
- package/lib/dispatch.d.ts.map +1 -0
- package/lib/dispatch.js +317 -0
- package/lib/dispatch.js.map +1 -0
- package/lib/engineering-context.d.ts +86 -0
- package/lib/engineering-context.d.ts.map +1 -0
- package/lib/engineering-context.js +263 -0
- package/lib/engineering-context.js.map +1 -0
- package/lib/evaluation.d.ts +125 -0
- package/lib/evaluation.d.ts.map +1 -0
- package/lib/evaluation.js +295 -0
- package/lib/evaluation.js.map +1 -0
- package/lib/event-follow.d.ts +93 -0
- package/lib/event-follow.d.ts.map +1 -0
- package/lib/event-follow.js +150 -0
- package/lib/event-follow.js.map +1 -0
- package/lib/feedback-targets.d.ts +91 -0
- package/lib/feedback-targets.d.ts.map +1 -0
- package/lib/feedback-targets.js +362 -0
- package/lib/feedback-targets.js.map +1 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +25 -0
- package/lib/index.js.map +1 -0
- package/lib/intake-admission.d.ts +49 -0
- package/lib/intake-admission.d.ts.map +1 -0
- package/lib/intake-admission.js +100 -0
- package/lib/intake-admission.js.map +1 -0
- package/lib/intake-evaluation-runtime.d.ts +74 -0
- package/lib/intake-evaluation-runtime.d.ts.map +1 -0
- package/lib/intake-evaluation-runtime.js +343 -0
- package/lib/intake-evaluation-runtime.js.map +1 -0
- package/lib/intake-evaluation.d.ts +180 -0
- package/lib/intake-evaluation.d.ts.map +1 -0
- package/lib/intake-evaluation.js +440 -0
- package/lib/intake-evaluation.js.map +1 -0
- package/lib/intake.d.ts +118 -0
- package/lib/intake.d.ts.map +1 -0
- package/lib/intake.js +137 -0
- package/lib/intake.js.map +1 -0
- package/lib/ledger.d.ts +86 -0
- package/lib/ledger.d.ts.map +1 -0
- package/lib/ledger.js +145 -0
- package/lib/ledger.js.map +1 -0
- package/lib/mandate.d.ts +45 -0
- package/lib/mandate.d.ts.map +1 -0
- package/lib/mandate.js +168 -0
- package/lib/mandate.js.map +1 -0
- package/lib/outcome.d.ts +137 -0
- package/lib/outcome.d.ts.map +1 -0
- package/lib/outcome.js +303 -0
- package/lib/outcome.js.map +1 -0
- package/lib/owner-access.d.ts +35 -0
- package/lib/owner-access.d.ts.map +1 -0
- package/lib/owner-access.js +152 -0
- package/lib/owner-access.js.map +1 -0
- package/lib/owner-continuation.d.ts +27 -0
- package/lib/owner-continuation.d.ts.map +1 -0
- package/lib/owner-continuation.js +131 -0
- package/lib/owner-continuation.js.map +1 -0
- package/lib/owner-recall.d.ts +85 -0
- package/lib/owner-recall.d.ts.map +1 -0
- package/lib/owner-recall.js +134 -0
- package/lib/owner-recall.js.map +1 -0
- package/lib/owner-replies.d.ts +44 -0
- package/lib/owner-replies.d.ts.map +1 -0
- package/lib/owner-replies.js +232 -0
- package/lib/owner-replies.js.map +1 -0
- package/lib/peers.d.ts +622 -0
- package/lib/peers.d.ts.map +1 -0
- package/lib/peers.js +10 -0
- package/lib/peers.js.map +1 -0
- package/lib/policy-notice.d.ts +31 -0
- package/lib/policy-notice.d.ts.map +1 -0
- package/lib/policy-notice.js +137 -0
- package/lib/policy-notice.js.map +1 -0
- package/lib/reply-contract.d.ts +67 -0
- package/lib/reply-contract.d.ts.map +1 -0
- package/lib/reply-contract.js +58 -0
- package/lib/reply-contract.js.map +1 -0
- package/lib/service.d.ts +625 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +2388 -0
- package/lib/service.js.map +1 -0
- package/lib/settlement.d.ts +74 -0
- package/lib/settlement.d.ts.map +1 -0
- package/lib/settlement.js +233 -0
- package/lib/settlement.js.map +1 -0
- package/lib/signals.d.ts +176 -0
- package/lib/signals.d.ts.map +1 -0
- package/lib/signals.js +497 -0
- package/lib/signals.js.map +1 -0
- package/lib/store.d.ts +789 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +3266 -0
- package/lib/store.js.map +1 -0
- package/lib/tools.d.ts +7 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +443 -0
- package/lib/tools.js.map +1 -0
- package/lib/types.d.ts +831 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +29 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +2 -0
- package/lib/version.js.map +1 -0
- package/lib/workspace.d.ts +43 -0
- package/lib/workspace.d.ts.map +1 -0
- package/lib/workspace.js +137 -0
- package/lib/workspace.js.map +1 -0
- package/package.json +101 -3
package/lib/signals.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic signal intake.
|
|
3
|
+
*
|
|
4
|
+
* Slice 1 only accepted signals that trusted Host code pushed with `admit()`.
|
|
5
|
+
* This module adds the missing read side: a small, bounded source-adapter
|
|
6
|
+
* boundary plus one concrete adapter over an *already authenticated* in-tree
|
|
7
|
+
* surface, so ordinary owner use produces work items without an operator
|
|
8
|
+
* inventing fixed scenarios.
|
|
9
|
+
*
|
|
10
|
+
* Contracts kept here:
|
|
11
|
+
*
|
|
12
|
+
* - Sources are read-only. A source never writes to delivery/automations; the
|
|
13
|
+
* only write path stays `WorkItemAdmission` + the existing dispatch path.
|
|
14
|
+
* - No timer. A sweep only runs when the host calls `sweepSignals()` or when a
|
|
15
|
+
* worker claims an item, and a persisted `sweepIntervalMs` gate bounds it.
|
|
16
|
+
* - Missing optional peers degrade to a no-op report, never to an activation
|
|
17
|
+
* or claim failure.
|
|
18
|
+
* - Every candidate carries provenance and a canonical key; the ledger's dedup
|
|
19
|
+
* window plus the settled-item fence in admission decide what becomes work.
|
|
20
|
+
*/
|
|
21
|
+
import type { NormalizedConfig } from './config.ts';
|
|
22
|
+
import type { WorkItemAdmission } from './admission.ts';
|
|
23
|
+
import type { WorkItemDispatch } from './dispatch.ts';
|
|
24
|
+
import type { PeerDeliveryService } from './peers.ts';
|
|
25
|
+
import type { WorkItemStore } from './store.ts';
|
|
26
|
+
import type { WorkItemProvenance, WorkItemThreadRef } from './types.ts';
|
|
27
|
+
/** What one adapter sweep is allowed to spend. */
|
|
28
|
+
export interface WorkItemSignalSweepInput {
|
|
29
|
+
readonly now: number;
|
|
30
|
+
/** Maximum candidates to scan in this sweep. */
|
|
31
|
+
readonly limit: number;
|
|
32
|
+
/** Opaque cursor returned by the previous sweep of this source. */
|
|
33
|
+
readonly cursor: unknown;
|
|
34
|
+
}
|
|
35
|
+
/** One candidate an adapter believes is real, already-authenticated work. */
|
|
36
|
+
export interface WorkItemSignalCandidate {
|
|
37
|
+
readonly kind: string;
|
|
38
|
+
readonly externalId: string;
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly brief: string;
|
|
41
|
+
readonly originBindingId: string;
|
|
42
|
+
readonly acceptanceRef?: string;
|
|
43
|
+
/**
|
|
44
|
+
* External threads this work item follows. Optional: a source that does not
|
|
45
|
+
* model threads keeps admitting exactly as before.
|
|
46
|
+
*/
|
|
47
|
+
readonly follows?: readonly WorkItemThreadRef[];
|
|
48
|
+
readonly occurredAt: number;
|
|
49
|
+
readonly provenance: WorkItemProvenance;
|
|
50
|
+
/**
|
|
51
|
+
* `withdrawn` candidates are never admitted. A cancellation retires the open
|
|
52
|
+
* item the same canonical execution already produced, if one exists; a
|
|
53
|
+
* correction that does not match anything stays an observation.
|
|
54
|
+
*/
|
|
55
|
+
readonly disposition: 'failure' | 'timeout' | 'withdrawn';
|
|
56
|
+
}
|
|
57
|
+
/** The read-only contract every automatic source implements. */
|
|
58
|
+
export interface WorkItemSignalSource {
|
|
59
|
+
/** Stable lower-kebab id; also the provenance source name. */
|
|
60
|
+
readonly id: string;
|
|
61
|
+
/** Protocol/version of the underlying authenticated surface. */
|
|
62
|
+
readonly sourceVersion: string;
|
|
63
|
+
/** Read one bounded page. May throw; the intake contains the error. */
|
|
64
|
+
sweep(input: WorkItemSignalSweepInput): WorkItemSignalSweep;
|
|
65
|
+
}
|
|
66
|
+
export interface WorkItemSignalSweep {
|
|
67
|
+
readonly candidates: readonly WorkItemSignalCandidate[];
|
|
68
|
+
/** Cursor for the next sweep; `undefined` restarts from the oldest signal. */
|
|
69
|
+
readonly cursor: unknown;
|
|
70
|
+
/**
|
|
71
|
+
* Set when the source could not run this sweep (`missing-dependency`,
|
|
72
|
+
* `owner-route-unavailable`, `unsupported`). Never an exception.
|
|
73
|
+
*/
|
|
74
|
+
readonly degraded?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A source registration handed to the intake by the Host or by a peer plugin.
|
|
78
|
+
*
|
|
79
|
+
* Registration is the only way an out-of-tree producer joins automatic intake:
|
|
80
|
+
* the intake never imports a concrete peer, and a registered source is read on
|
|
81
|
+
* exactly the same bounded path as the built-in adapter. Budgets default to the
|
|
82
|
+
* configured sweep budget, so a deployment can tune one number for every source.
|
|
83
|
+
*/
|
|
84
|
+
export interface WorkItemSignalRegistration {
|
|
85
|
+
readonly source: WorkItemSignalSource;
|
|
86
|
+
/** Minimum interval between two sweeps of this source. */
|
|
87
|
+
readonly intervalMs?: number | undefined;
|
|
88
|
+
readonly maxCandidatesPerSweep?: number | undefined;
|
|
89
|
+
readonly maxAdmitsPerSweep?: number | undefined;
|
|
90
|
+
}
|
|
91
|
+
export type WorkItemSignalSkip = 'disabled' | 'not-due' | 'degraded';
|
|
92
|
+
/** One source's contribution to a sweep, kept small enough to log. */
|
|
93
|
+
export interface WorkItemSignalSourceReport {
|
|
94
|
+
readonly source: string;
|
|
95
|
+
readonly skipped: WorkItemSignalSkip | null;
|
|
96
|
+
readonly degraded: string | null;
|
|
97
|
+
readonly scanned: number;
|
|
98
|
+
readonly admitted: number;
|
|
99
|
+
readonly duplicates: number;
|
|
100
|
+
readonly fenced: number;
|
|
101
|
+
/** Withdrawn candidates observed in this sweep. */
|
|
102
|
+
readonly withdrawn: number;
|
|
103
|
+
/** Of those, items actually moved to `dropped` by this sweep. */
|
|
104
|
+
readonly retired: number;
|
|
105
|
+
readonly rejected: number;
|
|
106
|
+
readonly failed: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface WorkItemSignalSweepReport {
|
|
109
|
+
readonly sources: readonly WorkItemSignalSourceReport[];
|
|
110
|
+
}
|
|
111
|
+
export interface SignalIntakeDependencies {
|
|
112
|
+
readonly config: NormalizedConfig;
|
|
113
|
+
readonly store: WorkItemStore;
|
|
114
|
+
readonly admission: WorkItemAdmission;
|
|
115
|
+
/** Resolved lazily so a late-mounted `assistant-delivery` still works. */
|
|
116
|
+
readonly delivery?: (() => PeerDeliveryService | undefined) | undefined;
|
|
117
|
+
readonly dispatch?: WorkItemDispatch | undefined;
|
|
118
|
+
readonly now?: (() => number) | undefined;
|
|
119
|
+
readonly warn?: ((message: string) => void) | undefined;
|
|
120
|
+
/** Initial registrations supplied by the Host or a test; more may be added. */
|
|
121
|
+
readonly sources?: readonly WorkItemSignalSource[] | undefined;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Bounded sweep driver: gate → adapter → admission → persist cursor.
|
|
125
|
+
*
|
|
126
|
+
* The intake owns no timer and no queue. It is intentionally synchronous so it
|
|
127
|
+
* can be called from the worker's claim path without adding a background task.
|
|
128
|
+
*/
|
|
129
|
+
export declare class WorkItemSignalIntake {
|
|
130
|
+
#private;
|
|
131
|
+
constructor(dependencies: SignalIntakeDependencies);
|
|
132
|
+
/**
|
|
133
|
+
* Add one source and return an idempotent disposer.
|
|
134
|
+
*
|
|
135
|
+
* Registration is refused with `invalid-input` for a duplicate id or a
|
|
136
|
+
* malformed registration; a rejected registration leaves the intake intact.
|
|
137
|
+
*/
|
|
138
|
+
register(registration: WorkItemSignalRegistration): () => void;
|
|
139
|
+
/** Drop one source; `false` when it was never registered. */
|
|
140
|
+
unregister(id: string): boolean;
|
|
141
|
+
/** Registered source ids, in sweep order. */
|
|
142
|
+
sources(): readonly string[];
|
|
143
|
+
/** True when at least one adapter is armed, even if a peer is missing. */
|
|
144
|
+
armed(): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Sweep every armed source once.
|
|
147
|
+
*
|
|
148
|
+
* Never throws: adapter failures are contained per source, and admission
|
|
149
|
+
* rejections are counted as skips. `force` bypasses the persisted interval,
|
|
150
|
+
* which only the explicit host call does.
|
|
151
|
+
*/
|
|
152
|
+
sweep(force?: boolean): WorkItemSignalSweepReport;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Concrete adapter: the owner's own failed/timed-out foreground requests.
|
|
156
|
+
*
|
|
157
|
+
* Evidence (`file:line` at implementation time):
|
|
158
|
+
* - `plugins/assistant-delivery/src/service.ts:5098` declares
|
|
159
|
+
* `listOwnerForegroundTaskSources`, owner-route authenticated and read-only.
|
|
160
|
+
* - `plugins/assistant-delivery/src/types.ts` defines the source/page protocol
|
|
161
|
+
* (`assistant-delivery/owner-foreground-source/v1`).
|
|
162
|
+
* - `plugins/assistant-delivery/src/acceptance.ts:20` defines the execution
|
|
163
|
+
* statuses used here.
|
|
164
|
+
*
|
|
165
|
+
* It only reads: the page already carries authenticated owner identity,
|
|
166
|
+
* canonical digests and a completion cursor. Title/brief are derived from that
|
|
167
|
+
* metadata, never from raw owner text (which is not part of this page).
|
|
168
|
+
*/
|
|
169
|
+
export declare class OwnerForegroundFailureSource implements WorkItemSignalSource {
|
|
170
|
+
#private;
|
|
171
|
+
readonly id = "owner-foreground-failures";
|
|
172
|
+
readonly sourceVersion = "assistant-delivery/owner-foreground-source/v1";
|
|
173
|
+
constructor(config: NormalizedConfig, delivery: PeerDeliveryService);
|
|
174
|
+
sweep(input: WorkItemSignalSweepInput): WorkItemSignalSweep;
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=signals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signals.d.ts","sourceRoot":"","sources":["../src/signals.ts"],"names":[],"mappings":"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"}
|
package/lib/signals.js
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic signal intake.
|
|
3
|
+
*
|
|
4
|
+
* Slice 1 only accepted signals that trusted Host code pushed with `admit()`.
|
|
5
|
+
* This module adds the missing read side: a small, bounded source-adapter
|
|
6
|
+
* boundary plus one concrete adapter over an *already authenticated* in-tree
|
|
7
|
+
* surface, so ordinary owner use produces work items without an operator
|
|
8
|
+
* inventing fixed scenarios.
|
|
9
|
+
*
|
|
10
|
+
* Contracts kept here:
|
|
11
|
+
*
|
|
12
|
+
* - Sources are read-only. A source never writes to delivery/automations; the
|
|
13
|
+
* only write path stays `WorkItemAdmission` + the existing dispatch path.
|
|
14
|
+
* - No timer. A sweep only runs when the host calls `sweepSignals()` or when a
|
|
15
|
+
* worker claims an item, and a persisted `sweepIntervalMs` gate bounds it.
|
|
16
|
+
* - Missing optional peers degrade to a no-op report, never to an activation
|
|
17
|
+
* or claim failure.
|
|
18
|
+
* - Every candidate carries provenance and a canonical key; the ledger's dedup
|
|
19
|
+
* window plus the settled-item fence in admission decide what becomes work.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { isActive } from "./config.js";
|
|
23
|
+
import { WorkItemError } from "./types.js";
|
|
24
|
+
/** Must be a subset of the provenance shape admission accepts (`admission.ts`). */
|
|
25
|
+
const SIGNAL_SOURCE_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
26
|
+
/** Id of the adapter armed by `sources.foregroundFailures`; a registration may shadow it. */
|
|
27
|
+
const BUILT_IN_SOURCE_ID = 'owner-foreground-failures';
|
|
28
|
+
const CURSOR_KEY_PREFIX = 'signal.cursor:';
|
|
29
|
+
const SWEPT_KEY_PREFIX = 'signal.sweptAt:';
|
|
30
|
+
const REJECTION_CODES = new Set(['cooldown', 'rate-limited', 'capacity', 'kind-rejected', 'source-rejected']);
|
|
31
|
+
function sha256(value) {
|
|
32
|
+
return createHash('sha256').update(value).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
function emptyReport(source, skipped, degraded = null) {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
source,
|
|
37
|
+
skipped,
|
|
38
|
+
degraded,
|
|
39
|
+
scanned: 0,
|
|
40
|
+
admitted: 0,
|
|
41
|
+
duplicates: 0,
|
|
42
|
+
fenced: 0,
|
|
43
|
+
withdrawn: 0,
|
|
44
|
+
retired: 0,
|
|
45
|
+
rejected: 0,
|
|
46
|
+
failed: false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bounded sweep driver: gate → adapter → admission → persist cursor.
|
|
51
|
+
*
|
|
52
|
+
* The intake owns no timer and no queue. It is intentionally synchronous so it
|
|
53
|
+
* can be called from the worker's claim path without adding a background task.
|
|
54
|
+
*/
|
|
55
|
+
export class WorkItemSignalIntake {
|
|
56
|
+
#deps;
|
|
57
|
+
#now;
|
|
58
|
+
#warn;
|
|
59
|
+
/** Insertion-ordered registrations, so reports keep a stable order. */
|
|
60
|
+
#registered = new Map();
|
|
61
|
+
constructor(dependencies) {
|
|
62
|
+
this.#deps = dependencies;
|
|
63
|
+
this.#now = dependencies.now ?? (() => Date.now());
|
|
64
|
+
this.#warn = dependencies.warn ?? (() => { });
|
|
65
|
+
for (const source of dependencies.sources ?? [])
|
|
66
|
+
this.register({ source });
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Add one source and return an idempotent disposer.
|
|
70
|
+
*
|
|
71
|
+
* Registration is refused with `invalid-input` for a duplicate id or a
|
|
72
|
+
* malformed registration; a rejected registration leaves the intake intact.
|
|
73
|
+
*/
|
|
74
|
+
register(registration) {
|
|
75
|
+
const resolved = this.#resolve(registration);
|
|
76
|
+
const id = resolved.source.id;
|
|
77
|
+
if (this.#registered.has(id)) {
|
|
78
|
+
throw new WorkItemError('invalid-input', `the signal source ${id} is already registered`);
|
|
79
|
+
}
|
|
80
|
+
this.#registered.set(id, resolved);
|
|
81
|
+
return () => {
|
|
82
|
+
// Only retire this exact registration: a stale disposer from a replaced
|
|
83
|
+
// provider must not unregister a newer one that reuses the id.
|
|
84
|
+
if (this.#registered.get(id) === resolved)
|
|
85
|
+
this.unregister(id);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Drop one source; `false` when it was never registered. */
|
|
89
|
+
unregister(id) {
|
|
90
|
+
return this.#registered.delete(id);
|
|
91
|
+
}
|
|
92
|
+
/** Registered source ids, in sweep order. */
|
|
93
|
+
sources() {
|
|
94
|
+
return Object.freeze([...this.#registered.keys()]);
|
|
95
|
+
}
|
|
96
|
+
/** True when at least one adapter is armed, even if a peer is missing. */
|
|
97
|
+
armed() {
|
|
98
|
+
if (this.#registered.size > 0)
|
|
99
|
+
return true;
|
|
100
|
+
return this.#deps.config.sources.foregroundFailures.enabled;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Sweep every armed source once.
|
|
104
|
+
*
|
|
105
|
+
* Never throws: adapter failures are contained per source, and admission
|
|
106
|
+
* rejections are counted as skips. `force` bypasses the persisted interval,
|
|
107
|
+
* which only the explicit host call does.
|
|
108
|
+
*/
|
|
109
|
+
sweep(force = false) {
|
|
110
|
+
const config = this.#deps.config;
|
|
111
|
+
if (!config.enabled || !this.armed())
|
|
112
|
+
return Object.freeze({ sources: [] });
|
|
113
|
+
const sources = this.#sources();
|
|
114
|
+
const reports = [];
|
|
115
|
+
for (const source of sources) {
|
|
116
|
+
try {
|
|
117
|
+
reports.push(this.#sweepSource(source, force));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// A programming error in one adapter must not take down a claim.
|
|
121
|
+
this.#warn(`assistant-work-items: signal source ${source.source.id} failed: ${describe(error)}`);
|
|
122
|
+
reports.push(Object.freeze({
|
|
123
|
+
...emptyReport(source.source.id, null),
|
|
124
|
+
failed: true,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return Object.freeze({ sources: Object.freeze(reports) });
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The registered sources, then the built-in adapter when the config arms it.
|
|
132
|
+
*
|
|
133
|
+
* The built-in adapter still resolves `assistant-delivery` lazily and skips
|
|
134
|
+
* itself when that peer is absent, so a late-mounted peer stays a no-op
|
|
135
|
+
* instead of an activation failure. A registration may shadow the built-in
|
|
136
|
+
* adapter by reusing its id.
|
|
137
|
+
*/
|
|
138
|
+
#sources() {
|
|
139
|
+
const resolved = [...this.#registered.values()];
|
|
140
|
+
const config = this.#deps.config;
|
|
141
|
+
if (!config.sources.foregroundFailures.enabled || this.#registered.has(BUILT_IN_SOURCE_ID))
|
|
142
|
+
return resolved;
|
|
143
|
+
const delivery = this.#deps.delivery?.();
|
|
144
|
+
if (delivery === undefined)
|
|
145
|
+
return resolved;
|
|
146
|
+
resolved.push(Object.freeze({
|
|
147
|
+
source: new OwnerForegroundFailureSource(config, delivery),
|
|
148
|
+
intervalMs: config.sources.foregroundFailures.sweepIntervalMs,
|
|
149
|
+
maxCandidatesPerSweep: config.sources.foregroundFailures.maxCandidatesPerSweep,
|
|
150
|
+
maxAdmitsPerSweep: config.sources.foregroundFailures.maxAdmitsPerSweep,
|
|
151
|
+
}));
|
|
152
|
+
return resolved;
|
|
153
|
+
}
|
|
154
|
+
/** Validate one registration; throws `invalid-input` instead of half-applying. */
|
|
155
|
+
#resolve(registration) {
|
|
156
|
+
const source = registration?.source;
|
|
157
|
+
const id = source?.id;
|
|
158
|
+
if (typeof id !== 'string' || !SIGNAL_SOURCE_ID.test(id)) {
|
|
159
|
+
throw new WorkItemError('invalid-input', 'a signal source id must be lower-kebab-case and at most 64 bytes');
|
|
160
|
+
}
|
|
161
|
+
if (typeof source?.sourceVersion !== 'string' || source.sourceVersion.length === 0) {
|
|
162
|
+
throw new WorkItemError('invalid-input', `the signal source ${id} needs a non-empty sourceVersion`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof source?.sweep !== 'function') {
|
|
165
|
+
throw new WorkItemError('invalid-input', `the signal source ${id} needs a sweep function`);
|
|
166
|
+
}
|
|
167
|
+
const defaults = this.#deps.config.sources.foregroundFailures;
|
|
168
|
+
const pick = (value, fallback, field) => {
|
|
169
|
+
if (value === undefined)
|
|
170
|
+
return fallback;
|
|
171
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
172
|
+
throw new WorkItemError('invalid-input', `the signal source ${id} has an invalid ${field}`);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
};
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
source,
|
|
178
|
+
intervalMs: pick(registration.intervalMs, defaults.sweepIntervalMs, 'intervalMs'),
|
|
179
|
+
maxCandidatesPerSweep: pick(registration.maxCandidatesPerSweep, defaults.maxCandidatesPerSweep, 'maxCandidatesPerSweep'),
|
|
180
|
+
maxAdmitsPerSweep: pick(registration.maxAdmitsPerSweep, defaults.maxAdmitsPerSweep, 'maxAdmitsPerSweep'),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
#sweepSource(resolved, force) {
|
|
184
|
+
const config = this.#deps.config;
|
|
185
|
+
const store = this.#deps.store;
|
|
186
|
+
const now = this.#now();
|
|
187
|
+
const source = resolved.source;
|
|
188
|
+
const sweptAtRaw = store.meta(`${SWEPT_KEY_PREFIX}${source.id}`);
|
|
189
|
+
const sweptAt = sweptAtRaw === null ? null : Number(sweptAtRaw);
|
|
190
|
+
if (!force && sweptAt !== null && Number.isFinite(sweptAt) && now - sweptAt < resolved.intervalMs) {
|
|
191
|
+
return emptyReport(source.id, 'not-due');
|
|
192
|
+
}
|
|
193
|
+
// Mark the attempt before it starts: a permanently failing adapter must
|
|
194
|
+
// still be bounded by the interval instead of retrying on every claim.
|
|
195
|
+
store.setMeta(`${SWEPT_KEY_PREFIX}${source.id}`, String(now));
|
|
196
|
+
let result;
|
|
197
|
+
try {
|
|
198
|
+
result = source.sweep({
|
|
199
|
+
now,
|
|
200
|
+
limit: resolved.maxCandidatesPerSweep,
|
|
201
|
+
cursor: this.#readCursor(source.id),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
// Cursor epoch/scope changes make delivery reject the old cursor. Reset
|
|
206
|
+
// it so the next sweep restarts at the oldest signal; dedupe makes the
|
|
207
|
+
// overlap harmless.
|
|
208
|
+
store.setMeta(`${CURSOR_KEY_PREFIX}${source.id}`, '');
|
|
209
|
+
this.#warn(`assistant-work-items: signal source ${source.id} sweep failed: ${describe(error)}`);
|
|
210
|
+
return Object.freeze({ ...emptyReport(source.id, null), failed: true });
|
|
211
|
+
}
|
|
212
|
+
if (result.degraded !== undefined) {
|
|
213
|
+
return emptyReport(source.id, 'degraded', result.degraded);
|
|
214
|
+
}
|
|
215
|
+
let scanned = 0;
|
|
216
|
+
let admitted = 0;
|
|
217
|
+
let duplicates = 0;
|
|
218
|
+
let fenced = 0;
|
|
219
|
+
let withdrawn = 0;
|
|
220
|
+
let retired = 0;
|
|
221
|
+
let rejected = 0;
|
|
222
|
+
for (const candidate of result.candidates) {
|
|
223
|
+
scanned += 1;
|
|
224
|
+
// A source may only speak for itself, and this check runs before the
|
|
225
|
+
// withdrawal branch: cross-attribution would otherwise let one source
|
|
226
|
+
// cancel another source's work, not just launder its provenance.
|
|
227
|
+
if (candidate.provenance?.source !== source.id) {
|
|
228
|
+
rejected += 1;
|
|
229
|
+
this.#warn(`assistant-work-items: signal source ${source.id} dropped a candidate attributed to ${String(candidate.provenance?.source)}`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (candidate.disposition === 'withdrawn') {
|
|
233
|
+
withdrawn += 1;
|
|
234
|
+
let withdrawal;
|
|
235
|
+
try {
|
|
236
|
+
withdrawal = this.#deps.admission.withdraw({
|
|
237
|
+
kind: candidate.kind,
|
|
238
|
+
externalId: candidate.externalId,
|
|
239
|
+
title: candidate.title,
|
|
240
|
+
brief: candidate.brief,
|
|
241
|
+
originBindingId: candidate.originBindingId,
|
|
242
|
+
...(candidate.acceptanceRef === undefined ? {} : { acceptanceRef: candidate.acceptanceRef }),
|
|
243
|
+
provenance: candidate.provenance,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error instanceof WorkItemError && REJECTION_CODES.has(error.code)) {
|
|
248
|
+
rejected += 1;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
this.#warn(`assistant-work-items: withdrawal of a ${source.id} signal failed: ${describe(error)}`);
|
|
252
|
+
rejected += 1;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// `absent` (never admitted) and `settled` (already terminal) stay
|
|
256
|
+
// observations; only real transitions are retirements, and one signal
|
|
257
|
+
// can retire several open rows when the dedup window rolled between
|
|
258
|
+
// admissions. A withdrawal never wakes the worker.
|
|
259
|
+
if (withdrawal.outcome === 'withdrawn')
|
|
260
|
+
retired += withdrawal.items.length;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (admitted >= resolved.maxAdmitsPerSweep) {
|
|
264
|
+
// Budget is finite per sweep; the cursor still advances so the next
|
|
265
|
+
// sweep sees a different slice instead of the same head forever.
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
let outcome;
|
|
269
|
+
try {
|
|
270
|
+
outcome = this.#deps.admission.admit({
|
|
271
|
+
kind: candidate.kind,
|
|
272
|
+
externalId: candidate.externalId,
|
|
273
|
+
title: candidate.title,
|
|
274
|
+
brief: candidate.brief,
|
|
275
|
+
originBindingId: candidate.originBindingId,
|
|
276
|
+
...(candidate.acceptanceRef === undefined ? {} : { acceptanceRef: candidate.acceptanceRef }),
|
|
277
|
+
...(candidate.follows === undefined ? {} : { follows: candidate.follows }),
|
|
278
|
+
provenance: candidate.provenance,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (error instanceof WorkItemError && REJECTION_CODES.has(error.code)) {
|
|
283
|
+
rejected += 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
this.#warn(`assistant-work-items: admission of a ${source.id} signal failed: ${describe(error)}`);
|
|
287
|
+
rejected += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!outcome.created) {
|
|
291
|
+
if (outcome.reason === 'fenced')
|
|
292
|
+
fenced += 1;
|
|
293
|
+
else
|
|
294
|
+
duplicates += 1;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
admitted += 1;
|
|
298
|
+
if (isActive(config))
|
|
299
|
+
this.#wake(outcome.item);
|
|
300
|
+
}
|
|
301
|
+
this.#writeCursor(source.id, result.cursor);
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
source: source.id,
|
|
304
|
+
skipped: null,
|
|
305
|
+
degraded: null,
|
|
306
|
+
scanned,
|
|
307
|
+
admitted,
|
|
308
|
+
duplicates,
|
|
309
|
+
fenced,
|
|
310
|
+
withdrawn,
|
|
311
|
+
retired,
|
|
312
|
+
rejected,
|
|
313
|
+
failed: false,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
#wake(item) {
|
|
317
|
+
const dispatch = this.#deps.dispatch;
|
|
318
|
+
if (dispatch === undefined)
|
|
319
|
+
return;
|
|
320
|
+
try {
|
|
321
|
+
dispatch.wake(item);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
this.#warn(`assistant-work-items: worker wake failed: ${describe(error)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
#readCursor(source) {
|
|
328
|
+
const raw = this.#deps.store.meta(`${CURSOR_KEY_PREFIX}${source}`);
|
|
329
|
+
if (raw === null || raw === '')
|
|
330
|
+
return undefined;
|
|
331
|
+
try {
|
|
332
|
+
return JSON.parse(raw);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
#writeCursor(source, cursor) {
|
|
339
|
+
if (cursor === undefined || cursor === null) {
|
|
340
|
+
this.#deps.store.setMeta(`${CURSOR_KEY_PREFIX}${source}`, '');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
this.#deps.store.setMeta(`${CURSOR_KEY_PREFIX}${source}`, JSON.stringify(cursor));
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
this.#warn(`assistant-work-items: could not persist the ${source} cursor: ${describe(error)}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Concrete adapter: the owner's own failed/timed-out foreground requests.
|
|
353
|
+
*
|
|
354
|
+
* Evidence (`file:line` at implementation time):
|
|
355
|
+
* - `plugins/assistant-delivery/src/service.ts:5098` declares
|
|
356
|
+
* `listOwnerForegroundTaskSources`, owner-route authenticated and read-only.
|
|
357
|
+
* - `plugins/assistant-delivery/src/types.ts` defines the source/page protocol
|
|
358
|
+
* (`assistant-delivery/owner-foreground-source/v1`).
|
|
359
|
+
* - `plugins/assistant-delivery/src/acceptance.ts:20` defines the execution
|
|
360
|
+
* statuses used here.
|
|
361
|
+
*
|
|
362
|
+
* It only reads: the page already carries authenticated owner identity,
|
|
363
|
+
* canonical digests and a completion cursor. Title/brief are derived from that
|
|
364
|
+
* metadata, never from raw owner text (which is not part of this page).
|
|
365
|
+
*/
|
|
366
|
+
export class OwnerForegroundFailureSource {
|
|
367
|
+
id = BUILT_IN_SOURCE_ID;
|
|
368
|
+
sourceVersion = 'assistant-delivery/owner-foreground-source/v1';
|
|
369
|
+
#config;
|
|
370
|
+
#delivery;
|
|
371
|
+
constructor(config, delivery) {
|
|
372
|
+
this.#config = config;
|
|
373
|
+
this.#delivery = delivery;
|
|
374
|
+
}
|
|
375
|
+
sweep(input) {
|
|
376
|
+
const list = this.#delivery.listOwnerForegroundTaskSources;
|
|
377
|
+
if (list === undefined)
|
|
378
|
+
return { candidates: [], cursor: input.cursor, degraded: 'unsupported' };
|
|
379
|
+
const scope = this.#config.scope;
|
|
380
|
+
if (scope === null)
|
|
381
|
+
return { candidates: [], cursor: input.cursor, degraded: 'owner-route-unavailable' };
|
|
382
|
+
let receipt;
|
|
383
|
+
try {
|
|
384
|
+
receipt = this.#delivery.validateOwnerRoute({
|
|
385
|
+
authorityId: scope.ownerRouteId,
|
|
386
|
+
principalId: scope.principalId,
|
|
387
|
+
workspace: scope.workspace,
|
|
388
|
+
agentPreset: scope.preset,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return { candidates: [], cursor: input.cursor, degraded: 'owner-route-unavailable' };
|
|
393
|
+
}
|
|
394
|
+
const page = list.call(this.#delivery, {
|
|
395
|
+
authorityId: scope.ownerRouteId,
|
|
396
|
+
principalId: scope.principalId,
|
|
397
|
+
workspace: scope.workspace,
|
|
398
|
+
agentPreset: scope.preset,
|
|
399
|
+
expectedOwner: {
|
|
400
|
+
authorityHash: receipt.authorityHash,
|
|
401
|
+
principalRecordId: receipt.principalRecordId,
|
|
402
|
+
principalVersion: receipt.principalVersion,
|
|
403
|
+
},
|
|
404
|
+
...(input.cursor === undefined ? {} : { after: input.cursor }),
|
|
405
|
+
limit: input.limit,
|
|
406
|
+
});
|
|
407
|
+
const candidates = [];
|
|
408
|
+
for (const source of page.items) {
|
|
409
|
+
const candidate = this.#candidate(source, receipt.authorityHash, input);
|
|
410
|
+
if (candidate !== null)
|
|
411
|
+
candidates.push(candidate);
|
|
412
|
+
}
|
|
413
|
+
const cursor = page.hasMore ? page.nextCursor : undefined;
|
|
414
|
+
return { candidates, cursor };
|
|
415
|
+
}
|
|
416
|
+
#candidate(source, authorityHash, input) {
|
|
417
|
+
const scope = this.#config.scope;
|
|
418
|
+
if (scope === null)
|
|
419
|
+
return null;
|
|
420
|
+
// Re-anchor every row to the configured scope and to the receipt we just
|
|
421
|
+
// validated; a page from another owner is discarded, not admitted.
|
|
422
|
+
if (source.scope.workspace !== scope.workspace || source.scope.preset !== scope.preset)
|
|
423
|
+
return null;
|
|
424
|
+
if (source.principalId !== scope.principalId || source.authorityHash !== authorityHash)
|
|
425
|
+
return null;
|
|
426
|
+
const config = this.#config.sources.foregroundFailures;
|
|
427
|
+
const status = source.execution.status;
|
|
428
|
+
const disposition = status === 'failed'
|
|
429
|
+
? 'failure'
|
|
430
|
+
: status === 'timed-out'
|
|
431
|
+
? (config.includeTimedOut ? 'timeout' : null)
|
|
432
|
+
: status === 'cancelled'
|
|
433
|
+
? 'withdrawn'
|
|
434
|
+
: null;
|
|
435
|
+
if (disposition === null)
|
|
436
|
+
return null;
|
|
437
|
+
const completedAt = source.execution.completedAt;
|
|
438
|
+
if (typeof completedAt !== 'number' || !Number.isSafeInteger(completedAt) || completedAt <= 0)
|
|
439
|
+
return null;
|
|
440
|
+
const bindingId = source.binding.id;
|
|
441
|
+
if (bindingId === '')
|
|
442
|
+
return null;
|
|
443
|
+
// Canonical key over durable, authenticated identities (not the text and
|
|
444
|
+
// deliberately not the disposition): the key is the identity of one
|
|
445
|
+
// execution, so a `failed` revision and its later `cancelled` revision of
|
|
446
|
+
// the same executionRef map to the same item. Titles/briefs still differ
|
|
447
|
+
// per disposition; only the identity is status-independent.
|
|
448
|
+
const canonical = sha256([
|
|
449
|
+
this.id,
|
|
450
|
+
source.inboxId,
|
|
451
|
+
bindingId,
|
|
452
|
+
String(source.binding.version),
|
|
453
|
+
String(source.binding.generation),
|
|
454
|
+
source.execution.executionRef,
|
|
455
|
+
].join('\u0000'));
|
|
456
|
+
const subject = disposition === 'withdrawn' ? 'cancelled' : disposition === 'timeout' ? 'timed-out' : 'failed';
|
|
457
|
+
const title = `Repair a ${subject} owner request`;
|
|
458
|
+
const brief = [
|
|
459
|
+
`The owner's foreground request in inbox ${source.inboxId} finished as ${subject}.`,
|
|
460
|
+
`Completed at ${new Date(completedAt).toISOString()} in workspace ${source.scope.workspace} (binding ${bindingId}, version ${String(source.binding.version)}, generation ${String(source.binding.generation)}).`,
|
|
461
|
+
'Inspect what this automation and its peer services actually recorded for the execution, work out what went wrong or what the owner still needs,',
|
|
462
|
+
'then make the smallest bounded repair you can justify and report exactly what you inspected, changed and could not change.',
|
|
463
|
+
'Never re-send a message, guess the owner intent, or act outside this workspace.',
|
|
464
|
+
].join(' ');
|
|
465
|
+
return Object.freeze({
|
|
466
|
+
kind: config.kind,
|
|
467
|
+
externalId: `owner-foreground:${canonical}`,
|
|
468
|
+
title,
|
|
469
|
+
brief,
|
|
470
|
+
originBindingId: bindingId,
|
|
471
|
+
occurredAt: completedAt,
|
|
472
|
+
disposition,
|
|
473
|
+
provenance: Object.freeze({
|
|
474
|
+
source: this.id,
|
|
475
|
+
sourceVersion: this.sourceVersion,
|
|
476
|
+
signalDigest: `sha256:${sha256(source.sourceDigest)}`,
|
|
477
|
+
observedAt: input.now,
|
|
478
|
+
evidence: Object.freeze({
|
|
479
|
+
inboxId: source.inboxId,
|
|
480
|
+
executionStatus: status,
|
|
481
|
+
completionSequence: source.completionSequence,
|
|
482
|
+
// Persisted so settlement can fence receipts against the owner route
|
|
483
|
+
// that admitted this execution, not against a same-pass value.
|
|
484
|
+
authorityHash: source.authorityHash,
|
|
485
|
+
bindingVersion: source.binding.version,
|
|
486
|
+
completedAt,
|
|
487
|
+
}),
|
|
488
|
+
}),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function describe(error) {
|
|
493
|
+
if (error instanceof Error)
|
|
494
|
+
return error.message;
|
|
495
|
+
return String(error);
|
|
496
|
+
}
|
|
497
|
+
//# sourceMappingURL=signals.js.map
|