@songsid/agend 2.3.3-beta.4 → 2.3.3-beta.5
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/dist/agent-cli.js +13 -112
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +0 -1
- package/dist/agent-endpoint.js +1 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +3 -6
- package/dist/agent-whats-new.js +7 -18
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +0 -7
- package/dist/auth-api.js +1 -15
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +106 -15
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +17 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-skills.d.ts +4 -6
- package/dist/bundled-skills.js +10 -14
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/mcp-server.js +5 -5
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +1 -3
- package/dist/channel/mcp-tools.js +4 -58
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +6 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +11 -3
- package/dist/daemon.js +59 -107
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-live.d.ts +1 -1
- package/dist/dashboard-live.js +3 -3
- package/dist/dashboard-live.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +1 -1
- package/dist/dashboard-permissions.js +1 -3
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/fleet-manager.d.ts +26 -44
- package/dist/fleet-manager.js +70 -226
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +26 -63
- package/dist/instance-lifecycle.js +1 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +19 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -62
- package/dist/outbound-schemas.js +2 -41
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +0 -9
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +2 -0
- package/dist/public-web-link.js +5 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/search/search-docs.d.ts +1 -1
- package/dist/search/search-docs.js +2 -2
- package/dist/search/search-docs.js.map +1 -1
- package/dist/settings-api.d.ts +6 -0
- package/dist/settings-api.js +14 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +3 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/tool-permissions.js +3 -10
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +6 -2
- package/dist/topic-commands.js +2 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/ui/chat-ambient.js +4 -4
- package/dist/ui/chat-commands.js +3 -2
- package/dist/ui/chat-store.js +4 -1
- package/dist/ui/chat-strings.js +6 -42
- package/dist/ui/chat-thread.js +6 -44
- package/dist/ui/panel-chat.js +21 -50
- package/dist/ui/preview-panel.js +3 -15
- package/dist/ui/search-dialog.js +30 -14
- package/dist/ui/settings-appearance.js +1 -3
- package/dist/ui/settings-guests.js +13 -4
- package/dist/ui/settings-learn.js +1 -1
- package/dist/ui/settings-mods.js +5 -12
- package/dist/ui/settings-registry.js +0 -1
- package/dist/ui/settings-strings.js +6 -6
- package/dist/ui/shared/app-prefs.js +17 -17
- package/dist/ui/shared/app-shell.js +12 -8
- package/dist/ui/shared/app-store.js +6 -2
- package/dist/ui/shared/app.css +4 -36
- package/dist/ui/shared/app.js +5 -4
- package/dist/ui/shared/instance-nav.js +9 -9
- package/dist/ui/shared/instance-switch.js +8 -2
- package/dist/ui/shared/panel-view.js +22 -8
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/usage/usage-api.d.ts +3 -0
- package/dist/usage/usage-api.js +5 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +2 -0
- package/dist/view-api.js +64 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +9 -17
- package/dist/web-api.js +255 -371
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +0 -2
- package/dist/web-appearance-tools.js +2 -5
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +0 -6
- package/dist/web-appearance-view.js +0 -1
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +14 -7
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +1 -17
- package/dist/web-asset-parts.js +1 -15
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +1 -7
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +3 -7
- package/dist/web-chat-history.js +10 -17
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +9 -1
- package/dist/web-commands.js +13 -0
- package/dist/web-commands.js.map +1 -1
- package/dist/web-experimental.d.ts +2 -11
- package/dist/web-experimental.js +8 -36
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-guest-access.d.ts +40 -2
- package/dist/web-guest-access.js +50 -14
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +2 -45
- package/dist/web-mod-bundle.js +15 -63
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-store.d.ts +3 -48
- package/dist/web-mod-store.js +30 -97
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +13 -0
- package/dist/web-prefs.js +38 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +1 -11
- package/dist/web-preview.js +6 -20
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +11 -3
- package/dist/web-session.js +22 -11
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -0
- package/dist/web-settings-patch.js +6 -3
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +0 -13
- package/dist/web-settings-tools.js +12 -84
- package/dist/web-settings-tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -672
- package/dist/ui/app-effects.js +0 -251
- package/dist/ui/chat-effect-posters.js +0 -42
- package/dist/ui/chat-effects.js +0 -167
- package/dist/ui/effect-trial-controller.js +0 -124
- package/dist/ui/effect-trial-offer.js +0 -2
- package/dist/ui/effect-trial-panel.js +0 -40
- package/dist/ui/effects-waker.js +0 -2
- package/dist/ui/settings-effects.js +0 -202
- package/dist/ui/settings-owner-effects.js +0 -82
- package/dist/ui/shared/effect-app-runtime.js +0 -299
- package/dist/ui/shared/effect-baseline.js +0 -64
- package/dist/ui/shared/effect-chat-runtime.js +0 -162
- package/dist/ui/shared/effect-cursor.js +0 -126
- package/dist/ui/shared/effect-display-metadata.js +0 -39
- package/dist/ui/shared/effect-frame.js +0 -146
- package/dist/ui/shared/effect-geometry.js +0 -137
- package/dist/ui/shared/effect-host-style.js +0 -10
- package/dist/ui/shared/effect-owner-regions.js +0 -74
- package/dist/ui/shared/effect-owner-source.js +0 -122
- package/dist/ui/shared/effect-policy.js +0 -151
- package/dist/ui/shared/effect-poster-reader.js +0 -98
- package/dist/ui/shared/effect-posters.js +0 -117
- package/dist/ui/shared/effect-profiler.js +0 -184
- package/dist/ui/shared/effect-profiles.js +0 -65
- package/dist/ui/shared/effect-registry.js +0 -545
- package/dist/ui/shared/effect-runtime-loader.js +0 -236
- package/dist/ui/shared/effect-safety.js +0 -153
- package/dist/ui/shared/effect-stock-snapshot.js +0 -43
- package/dist/ui/shared/effect-stock-trust.js +0 -217
- package/dist/ui/shared/effect-trial-shadow.js +0 -62
- package/dist/ui/shared/effect-trial-source.js +0 -103
- package/dist/ui/shared/effect-trial-stage.js +0 -170
- package/dist/ui/shared/effect-trial.css +0 -40
- package/dist/ui/shared/feature-flags.js +0 -127
- package/dist/ui/shared/owner-effect-prefs.js +0 -117
- package/dist/web-effect-bootstrap.d.ts +0 -12
- package/dist/web-effect-bootstrap.js +0 -49
- package/dist/web-effect-bootstrap.js.map +0 -1
- package/dist/web-effect-bundle.d.ts +0 -28
- package/dist/web-effect-bundle.js +0 -171
- package/dist/web-effect-bundle.js.map +0 -1
- package/dist/web-effect-canonical.d.ts +0 -8
- package/dist/web-effect-canonical.js +0 -27
- package/dist/web-effect-canonical.js.map +0 -1
- package/dist/web-effect-library.d.ts +0 -114
- package/dist/web-effect-library.js +0 -530
- package/dist/web-effect-library.js.map +0 -1
- package/dist/web-effect-playback.d.ts +0 -30
- package/dist/web-effect-playback.js +0 -71
- package/dist/web-effect-playback.js.map +0 -1
- package/dist/web-effect-poster.d.ts +0 -8
- package/dist/web-effect-poster.js +0 -52
- package/dist/web-effect-poster.js.map +0 -1
- package/dist/web-effect-profiles.d.ts +0 -118
- package/dist/web-effect-profiles.js +0 -181
- package/dist/web-effect-profiles.js.map +0 -1
- package/dist/web-effect-service.d.ts +0 -93
- package/dist/web-effect-service.js +0 -335
- package/dist/web-effect-service.js.map +0 -1
- package/dist/web-effect-tools.d.ts +0 -48
- package/dist/web-effect-tools.js +0 -129
- package/dist/web-effect-tools.js.map +0 -1
- package/dist/web-effect-trials.d.ts +0 -86
- package/dist/web-effect-trials.js +0 -204
- package/dist/web-effect-trials.js.map +0 -1
- package/dist/web-effect-upload.d.ts +0 -27
- package/dist/web-effect-upload.js +0 -185
- package/dist/web-effect-upload.js.map +0 -1
- package/dist/web-effect-worker.d.ts +0 -8
- package/dist/web-effect-worker.js +0 -170
- package/dist/web-effect-worker.js.map +0 -1
- package/dist/web-effect.d.ts +0 -103
- package/dist/web-effect.js +0 -153
- package/dist/web-effect.js.map +0 -1
- package/dist/web-experimental-http.d.ts +0 -2
- package/dist/web-experimental-http.js +0 -12
- package/dist/web-experimental-http.js.map +0 -1
- package/dist/web-mod-agent.d.ts +0 -50
- package/dist/web-mod-agent.js +0 -207
- package/dist/web-mod-agent.js.map +0 -1
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/** #1629: app-owned pool; Chat contributes a lease, never another registry/loader. */
|
|
2
|
-
import { effectPlaybackReason, EFFECT_BLOCK_REASONS } from "./effect-policy.js";
|
|
3
|
-
import { createEffectTrialSource } from "./effect-trial-source.js";
|
|
4
|
-
import { createOwnerEffectSource } from "./effect-owner-source.js";
|
|
5
|
-
import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
|
|
6
|
-
import { createEffectRegistry } from "./effect-registry.js";
|
|
7
|
-
import { createEffectSafety } from "./effect-safety.js";
|
|
8
|
-
import { stockEffectTrust } from "./effect-stock-trust.js";
|
|
9
|
-
import { createEffectProfileCollector } from "./effect-profiles.js";
|
|
10
|
-
import { effectPlaybackIdentity, createEffectWriter } from "./effect-chat-runtime.js";
|
|
11
|
-
|
|
12
|
-
const episodes = new WeakSet();
|
|
13
|
-
|
|
14
|
-
/** `current` belongs to the page/session episode. Route cleanup only releases its contribution. */
|
|
15
|
-
export function createAppEffectRuntime({ window: host, current, observe, onChange = () => {}, onDispose = () => {}, resolveOwnerTarget = null, onOwnerUnavailable = () => {}, factories = {} }) {
|
|
16
|
-
let dead = false, claimed = false, error = null, loader = null, registry = null, writer = null, source = null, personal = null, trial = null, pending = null, lastMetadata = null, serial = 0, revision = 0, stamp = null, syncing = false, queued = false;
|
|
17
|
-
const bindings = new Map(), profiles = new Map();
|
|
18
|
-
let lastContext = null, playbackUnavailable = null;
|
|
19
|
-
const identity = effectPlaybackIdentity(host), auth = host?.AgendAuth;
|
|
20
|
-
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
21
|
-
const alive = () => { if (dead) return false; const value = safe(() => current() === true && !auth.sessionEnded()); return !dead && value === true; };
|
|
22
|
-
const sourceAlive = s => {
|
|
23
|
-
if (!s || !alive() || s !== source || s.dead) return false;
|
|
24
|
-
const value = safe(() => s.current() === true && s.element.isConnected === true);
|
|
25
|
-
return !dead && s === source && !s.dead && value === true;
|
|
26
|
-
};
|
|
27
|
-
const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
|
|
28
|
-
const clock = () => host.performance.now();
|
|
29
|
-
let offAuth = null, disposed = null;
|
|
30
|
-
const snapshot = s => {
|
|
31
|
-
const effects = s?.metadata?.effects ?? [], known = registry?.snapshot?.() ?? [];
|
|
32
|
-
const rows = effects.map(p => {
|
|
33
|
-
const b = s.bindings.get(p.id), row = known.find(r => b && r.key === b.key && r.id === p.id);
|
|
34
|
-
if (row && !s.error && !playbackUnavailable) return row.reason == null && row.phase == null ? { ...row, phase: "loading" } : row;
|
|
35
|
-
return { id: p.id, key: b?.key ?? null, verdict: "off", phase: null, quality: null,
|
|
36
|
-
reason: s.error ?? playbackUnavailable ?? effectPlaybackReason(lastContext) ?? (!p.manifest.regions.includes("chat") ? "region_unavailable" : "runtime_unavailable") };
|
|
37
|
-
});
|
|
38
|
-
return { error: error ?? s?.error ?? null, rows, effects, loading: !!s && !s.dead && s.metadata === null && !s.error };
|
|
39
|
-
};
|
|
40
|
-
function tell() {
|
|
41
|
-
const s = source, value = snapshot(s); if (dead) return;
|
|
42
|
-
if (sourceAlive(s)) {
|
|
43
|
-
const next = JSON.stringify(value);
|
|
44
|
-
if (next !== s.lastSnapshot) { s.lastSnapshot = next; safe(() => s.onChange(value)); }
|
|
45
|
-
}
|
|
46
|
-
if (!dead) safe(() => onChange(snapshot(source)));
|
|
47
|
-
}
|
|
48
|
-
function reportPlayback(s) {
|
|
49
|
-
if (!sourceAlive(s) || identity.scope !== "owner") return;
|
|
50
|
-
const now = safe(clock);
|
|
51
|
-
if (!Number.isFinite(now) || now < 0 || s.reportClock !== null && now < s.reportClock) { s.reporter.dispose(); return; }
|
|
52
|
-
s.reportClock = now;
|
|
53
|
-
const value = snapshot(s);
|
|
54
|
-
for (const id of s.reported.keys()) if (!value.effects.some(p => p.id === id)) s.reported.delete(id);
|
|
55
|
-
for (const row of value.rows) {
|
|
56
|
-
if (!sourceAlive(s)) return;
|
|
57
|
-
const status = row.phase === "running" && row.reason == null ? "playing" : "blocked";
|
|
58
|
-
const reason = status === "playing" ? null : EFFECT_BLOCK_REASONS.includes(row.reason) ? row.reason : "unknown";
|
|
59
|
-
const device = lastContext?.constrained === true ? "constrained" : "standard";
|
|
60
|
-
const next = JSON.stringify([status, reason, device]), last = s.reported.get(row.id);
|
|
61
|
-
if (last && (now - last.at < 10000 || next === last.value && now - last.at < 60000)) continue;
|
|
62
|
-
s.reported.set(row.id, { at: now, value: next });
|
|
63
|
-
void s.reporter.post(`/ui/effects/profile/${row.id}?instance=${encodeURIComponent(s.name)}&ownerEpoch=${identity.epoch}`,
|
|
64
|
-
{ v: 1, kind: "playback", device, status, reason });
|
|
65
|
-
}
|
|
66
|
-
// No server polling or unbounded retries: at most one observation per effect / 10s, a 60s heartbeat.
|
|
67
|
-
if (sourceAlive(s)) s.reportTimer = host.setTimeout(() => reportPlayback(s), 10000);
|
|
68
|
-
}
|
|
69
|
-
function discard(s) {
|
|
70
|
-
if (!s || s.dead) return;
|
|
71
|
-
s.dead = true; s.generation++; s.metadata = null;
|
|
72
|
-
safe(() => host.clearTimeout(s.reportTimer)); safe(() => s.reporter?.dispose());
|
|
73
|
-
if (source === s) source = null;
|
|
74
|
-
safe(() => trial?.stopFor(s));
|
|
75
|
-
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
|
|
76
|
-
if (pending?.source === s) safe(() => pending.controller.abort());
|
|
77
|
-
revision++; stamp = null;
|
|
78
|
-
}
|
|
79
|
-
function dispose(restartable = false) {
|
|
80
|
-
if (dead) return disposed; dead = true; discard(source);
|
|
81
|
-
safe(() => trial?.dispose()); safe(() => pending?.controller.abort()); safe(() => registry?.dispose?.()); safe(() => loader?.dispose?.()); safe(() => personal?.dispose()); safe(() => writer?.dispose()); safe(() => offAuth?.());
|
|
82
|
-
safe(() => host?.removeEventListener?.("pagehide", dispose));
|
|
83
|
-
for (const row of profiles.values()) safe(() => row.collector.dispose()); profiles.clear(); bindings.clear(); safe(() => onDispose({ error }));
|
|
84
|
-
disposed = Promise.all([safe(() => loader?.whenDisposed?.()), safe(() => registry?.whenDisposed?.())]).then(() => { if (restartable === true && claimed) episodes.delete(host); });
|
|
85
|
-
return disposed;
|
|
86
|
-
}
|
|
87
|
-
function fail(reason) { if (dead) return; error = reason; dispose(); safe(() => onChange({ error, rows: [], effects: [], loading: false })); }
|
|
88
|
-
const unavailable = { ok: false, sync() {}, changedChat() {}, reply() {}, detachChat() {}, attachChat() { return null; }, attachTrial() { return null; }, stopTrial() {}, trialSnapshot: () => ({ rows: [] }), snapshot: () => snapshot(null), dispose };
|
|
89
|
-
if (!host || typeof host !== "object" || episodes.has(host) || !identity || typeof current !== "function" || typeof observe !== "function" || typeof auth?.onSessionEnd !== "function" || typeof auth?.sessionEnded !== "function") return unavailable;
|
|
90
|
-
episodes.add(host); claimed = true; // Claim before invoking current/auth callbacks: one attempt per Window episode.
|
|
91
|
-
if (!alive()) { dispose(); return unavailable; }
|
|
92
|
-
try {
|
|
93
|
-
offAuth = auth.onSessionEnd(dispose);
|
|
94
|
-
if (!alive()) { dispose(); safe(() => offAuth?.()); return unavailable; }
|
|
95
|
-
host.addEventListener?.("pagehide", dispose);
|
|
96
|
-
} catch { fail("runtime_unavailable"); return unavailable; }
|
|
97
|
-
const make = (key, fn) => factories[key] ?? fn;
|
|
98
|
-
function bindingAlive(b) { if (b?.kind === "trial") return trial?.current(b) === true; if (b?.kind === "owner") return personal?.current(b) === true; return !!b && sourceAlive(b.source) && b.source.generation === b.generation && bindings.get(b.key) === b && b.source.bindings.get(b.id) === b; }
|
|
99
|
-
const bindingFor = (key, id) => { const b = bindings.get(key) ?? trial?.binding(key, id) ?? personal?.binding(key, id); return b?.id === id && bindingAlive(b) ? b : null; };
|
|
100
|
-
try {
|
|
101
|
-
writer = createEffectWriter(host, alive);
|
|
102
|
-
loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive, onRevoke: () => fail("runtime_unavailable") });
|
|
103
|
-
if (!loader.ok || typeof loader.placementsSettled !== "function" || typeof loader.resourceSettled !== "function") throw Error("runtime_unavailable");
|
|
104
|
-
registry = make("registry", createEffectRegistry)({ window: host, origin: host.location?.origin, boot: host.document.body.dataset.effectBoot,
|
|
105
|
-
load: async (descriptor, signal) => {
|
|
106
|
-
const b = bindingFor(descriptor.key, descriptor.id);
|
|
107
|
-
if (!bindingAlive(b) || descriptor.id !== b.id || signal.aborted) return null;
|
|
108
|
-
if (b.kind === "trial") return trial.load(b, signal);
|
|
109
|
-
if (b.kind === "owner") return personal.load(b, signal);
|
|
110
|
-
const value = await loader.resourceSettled({ id: b.id, instance: b.source.name }, signal);
|
|
111
|
-
if (!bindingAlive(b) || signal.aborted) return null;
|
|
112
|
-
if (!value) { refuseSource(b.source); return null; }
|
|
113
|
-
return value;
|
|
114
|
-
},
|
|
115
|
-
safety: make("safety", createEffectSafety)({ storage: host.localStorage, crypto: host.crypto }),
|
|
116
|
-
stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
|
|
117
|
-
onChange() { try { tell(); emit(); } catch { fail("runtime_unavailable"); } },
|
|
118
|
-
onMount(owner) {
|
|
119
|
-
const b = bindingFor(owner.key, owner.id); if (identity.scope !== "owner" || !bindingAlive(b) || b.id !== owner.id || !owner.current()) return;
|
|
120
|
-
const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
|
|
121
|
-
const began = safe(clock);
|
|
122
|
-
if (collector.ok && Number.isFinite(began) && began >= 0 && bindingAlive(b) && owner.current()) profiles.set(owner, { binding: b, collector, began, attempted: null }); else collector.dispose?.();
|
|
123
|
-
},
|
|
124
|
-
onRetire(owner) { profiles.get(owner)?.collector.dispose(); profiles.delete(owner); },
|
|
125
|
-
onProfile(raw, owner) {
|
|
126
|
-
const row = profiles.get(owner); if (!row || !bindingAlive(row.binding) || !owner.current()) return;
|
|
127
|
-
row.collector.add(raw); if (row.binding.kind === "trial") return; // Diagnostic samples stay transient; shared safety still runs in registry.
|
|
128
|
-
const now = clock();
|
|
129
|
-
if (!Number.isFinite(now) || now < row.began || now - row.began < 10000 && raw.verdict !== "paused" || row.attempted !== null && now - row.attempted < 10000) return;
|
|
130
|
-
const report = row.collector.report(); if (!report || !bindingAlive(row.binding) || !owner.current()) return;
|
|
131
|
-
row.attempted = now;
|
|
132
|
-
const query = row.binding.kind === "owner" ? "" : `instance=${encodeURIComponent(row.binding.source.name)}&`;
|
|
133
|
-
void writer.post(`/ui/effects/profile/${owner.id}?${query}ownerEpoch=${identity.epoch}`, report.profile);
|
|
134
|
-
}, clock,
|
|
135
|
-
});
|
|
136
|
-
if (!registry.ok) {
|
|
137
|
-
// Keep authenticated placement metadata available for explanations. This inert adapter cannot create,
|
|
138
|
-
// load, resume, send to, or authorize a frame; a failed paint subsystem is never permission.
|
|
139
|
-
playbackUnavailable = "unsupported_host";
|
|
140
|
-
safe(() => registry?.dispose?.());
|
|
141
|
-
registry = { ok: true, frameRoot: null, update() {}, snapshot: () => [], publish() {}, resume: () => false, dispose() {} };
|
|
142
|
-
}
|
|
143
|
-
if (identity.scope === "owner") trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
|
|
144
|
-
if (!alive()) return false; const v = observe(), c = v.context;
|
|
145
|
-
return alive() && c.enabled === true && c.show === true && c.transportReady === true
|
|
146
|
-
&& c.safe === false && c.publicLink === false && c.reducedMotion === false && c.hidden === false;
|
|
147
|
-
}, ownerCurrent: (s, name) => sourceAlive(s) && s.name === name,
|
|
148
|
-
stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
|
|
149
|
-
readResource: (spec, signal) => typeof loader.trialResourceSettled === "function" ? loader.trialResourceSettled(spec, signal) : Promise.resolve(null),
|
|
150
|
-
onChange() { revision++; stamp = null; sync(); tell(); },
|
|
151
|
-
});
|
|
152
|
-
if (identity.scope === "owner" && typeof resolveOwnerTarget === "function") {
|
|
153
|
-
if (typeof loader.ownerSettled !== "function") throw Error("runtime_unavailable");
|
|
154
|
-
personal = createOwnerEffectSource({ current: alive,
|
|
155
|
-
readMetadata: signal => loader.ownerSettled(signal), readResource: (spec, signal) => loader.resourceSettled(spec, signal), resolveTarget: resolveOwnerTarget,
|
|
156
|
-
stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
|
|
157
|
-
onChange() { revision++; stamp = null; sync(); tell(); }, onUnavailable: (slot, id) => safe(() => onOwnerUnavailable(slot, id)),
|
|
158
|
-
});
|
|
159
|
-
if (!personal.ok) throw Error("runtime_unavailable");
|
|
160
|
-
}
|
|
161
|
-
} catch { fail("runtime_unavailable"); return unavailable; }
|
|
162
|
-
function descriptors(s) {
|
|
163
|
-
if (!sourceAlive(s) || s.error) return [];
|
|
164
|
-
return (s.metadata?.effects ?? []).filter(p => p.manifest.regions.includes("chat")).map((p, order) => {
|
|
165
|
-
let b = s.bindings.get(p.id); const identity = placementIdentity(p);
|
|
166
|
-
if (!b || b.identity !== identity) {
|
|
167
|
-
if (b) bindings.delete(b.key);
|
|
168
|
-
b = Object.freeze({ key: `chat:${s.serial}:${p.id}`, id: p.id, source: s, generation: s.generation, identity });
|
|
169
|
-
s.bindings.set(p.id, b); bindings.set(b.key, b);
|
|
170
|
-
}
|
|
171
|
-
return { key: b.key, id: p.id, by: p.by, region: "chat", layer: p.manifest.layer, order, paused: s.paused.get(p.id) === identity, visible: true, instance: s.name, params: p.params, element: s.element };
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
function observation() {
|
|
175
|
-
const value = observe();
|
|
176
|
-
return { ...value, context: { ...value.context, openInstance: sourceAlive(source) ? source.name : null } };
|
|
177
|
-
}
|
|
178
|
-
function emit() {
|
|
179
|
-
if (!alive() || !registry?.ok) return;
|
|
180
|
-
const s = source, value = observation();
|
|
181
|
-
if (sourceAlive(s)) { const state = safe(() => s.observe().state); if (sourceAlive(s) && ["idle", "working", "needs_you", "stopped"].includes(state)) registry.publish("state", { state }, s.name); }
|
|
182
|
-
if (alive() && ["light", "dark"].includes(value.mode)) registry.publish("theme", { mode: value.mode });
|
|
183
|
-
}
|
|
184
|
-
function refuseSource(s) {
|
|
185
|
-
if (!sourceAlive(s)) return;
|
|
186
|
-
s.error = "resource_unavailable"; s.generation++;
|
|
187
|
-
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear(); revision++; stamp = null; sync(); tell();
|
|
188
|
-
}
|
|
189
|
-
function queueSync() {
|
|
190
|
-
if (queued || dead) return; queued = true;
|
|
191
|
-
Promise.resolve().then(() => { queued = false; if (alive()) sync(); else dispose(); });
|
|
192
|
-
}
|
|
193
|
-
function sync() {
|
|
194
|
-
if (!alive()) { dispose(); return; } if (syncing) { queueSync(); return; }
|
|
195
|
-
syncing = true;
|
|
196
|
-
try {
|
|
197
|
-
const candidate = source;
|
|
198
|
-
if (candidate && !sourceAlive(candidate)) discard(candidate);
|
|
199
|
-
const before = revision, value = observation(), s = source;
|
|
200
|
-
if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
|
|
201
|
-
const entries = [...(personal?.descriptors() ?? []), ...(trial?.descriptors() ?? []), ...descriptors(s)];
|
|
202
|
-
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
203
|
-
lastContext = { ...value.context };
|
|
204
|
-
const next = JSON.stringify([revision, value.context, entries.map(d => [d.key, d.paused])]);
|
|
205
|
-
if (stamp !== next) {
|
|
206
|
-
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
207
|
-
stamp = next; registry.update(entries, value.context);
|
|
208
|
-
}
|
|
209
|
-
if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
|
|
210
|
-
emit();
|
|
211
|
-
const c = value.context;
|
|
212
|
-
// Metadata describes placements, not executable resources. Its existing session/epoch/current and
|
|
213
|
-
// shared settled-lane fences apply even when this viewer has declined playback. No code is fetched here.
|
|
214
|
-
if (!pending) {
|
|
215
|
-
const ownerNeeds = identity.scope === "owner" && !c.publicLink && c.enabled && c.show && c.transportReady && !c.safe && !c.reducedMotion && !c.hidden && personal?.needsMetadata() === true, chatNeeds = sourceAlive(s) && !s.error && !s.metadata;
|
|
216
|
-
if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
|
|
217
|
-
if (ownerNeeds && (!chatNeeds || lastMetadata !== "owner")) void readPersonal();
|
|
218
|
-
else if (chatNeeds) void read(s);
|
|
219
|
-
}
|
|
220
|
-
tell();
|
|
221
|
-
} catch { fail("runtime_unavailable"); }
|
|
222
|
-
finally { syncing = false; }
|
|
223
|
-
}
|
|
224
|
-
async function readPersonal() {
|
|
225
|
-
if (!personal || pending || !personal.needsMetadata() || !alive()) return;
|
|
226
|
-
const op = { source: personal, controller: new host.AbortController() }; pending = op; lastMetadata = "owner";
|
|
227
|
-
try { await personal.read(op.controller.signal); }
|
|
228
|
-
catch { fail("runtime_unavailable"); }
|
|
229
|
-
finally { if (pending === op) { pending = null; if (alive()) sync(); } }
|
|
230
|
-
}
|
|
231
|
-
function changedOwner() {
|
|
232
|
-
if (!personal || !alive()) return;
|
|
233
|
-
if (pending?.source === personal) safe(() => pending.controller.abort());
|
|
234
|
-
personal.invalidate();
|
|
235
|
-
}
|
|
236
|
-
function ownerSnapshot() {
|
|
237
|
-
return { ...(personal?.snapshot() ?? { error: null, rev: -1, loading: false, slots: {} }),
|
|
238
|
-
rows: registry?.snapshot().flatMap(row => { const b = personal?.binding(row.key, row.id); return b ? [{ ...row, slot: b.slot }] : []; }) ?? [] };
|
|
239
|
-
}
|
|
240
|
-
async function cursorImageSettled(selection, signal) {
|
|
241
|
-
const own = personal?.cursorSelection();
|
|
242
|
-
if (!alive() || !own || !personal.cursorCurrent(selection) || signal?.aborted) return null;
|
|
243
|
-
let value; try { value = await loader.cursorImageSettled(own.id, signal); } catch { value = null; }
|
|
244
|
-
if (!alive() || !personal.cursorCurrent(own) || !personal.cursorCurrent(selection) || signal?.aborted) return null;
|
|
245
|
-
if (!value) { personal.refuseCursor(own); return null; }
|
|
246
|
-
return value;
|
|
247
|
-
}
|
|
248
|
-
async function read(s) {
|
|
249
|
-
if (!sourceAlive(s) || pending || s.error) return;
|
|
250
|
-
const op = { source: s, generation: s.generation, controller: new host.AbortController() }; pending = op; lastMetadata = "chat";
|
|
251
|
-
try {
|
|
252
|
-
const answer = await loader.placementsSettled(s.name, op.controller.signal);
|
|
253
|
-
if (!sourceAlive(s) || op.generation !== s.generation || pending !== op || op.controller.signal.aborted) return;
|
|
254
|
-
if (!answer) { refuseSource(s); return; }
|
|
255
|
-
s.metadata = answer;
|
|
256
|
-
for (const [id, identity] of s.paused) if (!answer.effects.some(p => p.id === id && placementIdentity(p) === identity)) s.paused.delete(id);
|
|
257
|
-
revision++; stamp = null; sync(); tell();
|
|
258
|
-
} catch { if (sourceAlive(s) && op.generation === s.generation) refuseSource(s); }
|
|
259
|
-
finally { if (pending === op) { pending = null; if (alive()) sync(); } }
|
|
260
|
-
}
|
|
261
|
-
function changedSource(s) {
|
|
262
|
-
if (!sourceAlive(s) || s.error) return;
|
|
263
|
-
s.generation++; s.metadata = null;
|
|
264
|
-
for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
|
|
265
|
-
if (pending?.source === s) safe(() => pending.controller.abort()); revision++; stamp = null; sync(); tell();
|
|
266
|
-
}
|
|
267
|
-
function publishReply(s, event) { if (host.document.body.dataset.webTransport !== "poll" && sourceAlive(s) && !s.error && event.instance === s.name) registry.publish("reply", {}, s.name); }
|
|
268
|
-
function attachChat(options) {
|
|
269
|
-
discard(source); const expected = revision; sync();
|
|
270
|
-
if (!alive() || revision !== expected || source !== null) return null;
|
|
271
|
-
const { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange = () => {} } = options ?? {};
|
|
272
|
-
if (!alive() || typeof name !== "string" || name.length < 1 || name.length > 128 || !element || element.isConnected !== true || typeof ownCurrent !== "function" || typeof ownObserve !== "function" || safe(() => ownCurrent() === true) !== true || revision !== expected || source !== null) return null;
|
|
273
|
-
const s = { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange, serial: ++serial, generation: 0, dead: false, error: null, metadata: null, bindings: new Map(), paused: new Map() };
|
|
274
|
-
source = s;
|
|
275
|
-
s.reporter = createEffectWriter(host, () => sourceAlive(s)); s.reported = new Map(); s.reportClock = null;
|
|
276
|
-
s.reportTimer = host.setTimeout(() => reportPlayback(s), 10000);
|
|
277
|
-
revision++; stamp = null;
|
|
278
|
-
const placement = id => sourceAlive(s) && !s.error ? s.metadata?.effects.find(p => p.id === id) : null;
|
|
279
|
-
const changed = () => changedSource(s);
|
|
280
|
-
const api = { ok: true, sync,
|
|
281
|
-
changed,
|
|
282
|
-
reply(event) { publishReply(s, event); },
|
|
283
|
-
pause(id) { const p = placement(id); if (!p) return false; s.paused.set(id, placementIdentity(p)); revision++; sync(); return true; },
|
|
284
|
-
resume(id) { const p = placement(id); if (!p) return false; s.paused.delete(id); revision++; sync(); const b = s.bindings.get(id); return bindingAlive(b) && registry.resume(b.key); },
|
|
285
|
-
async remove(id) { const p = placement(id); if (identity.scope !== "owner" || !p) return false; const generation = s.generation; const ok = await writer.post("/ui/effects/remove", { instance: s.name, id }); if (!sourceAlive(s) || generation !== s.generation) return false; if (ok) changed(); return ok; },
|
|
286
|
-
snapshot: () => sourceAlive(s) ? snapshot(s) : { error: null, rows: [], effects: [], loading: false },
|
|
287
|
-
dispose() { if (source !== s) return; discard(s); sync(); },
|
|
288
|
-
};
|
|
289
|
-
sync(); tell(); return sourceAlive(s) ? api : null;
|
|
290
|
-
}
|
|
291
|
-
return { ok: true, playbackAvailable: playbackUnavailable === null, sync, attachChat, changedOwner, ownerSnapshot,
|
|
292
|
-
attachTrial(options) { const s = source; return trial && !playbackUnavailable && sourceAlive(s) ? trial.attach(options, s) : null; },
|
|
293
|
-
stopTrial() { trial?.stop(); },
|
|
294
|
-
trialSnapshot() { return { rows: registry?.snapshot().filter(row => trial?.binding(row.key, row.id)) ?? [] }; },
|
|
295
|
-
cursorSelection: () => personal?.cursorSelection() ?? null, cursorImageSettled,
|
|
296
|
-
pauseOwner(slot) { return personal?.pause(slot) === true; },
|
|
297
|
-
resumeOwner(slot) { if (!personal?.unpause(slot)) return false; const b = personal.slotBinding(slot); return bindingAlive(b) && registry.resume(b.key); },
|
|
298
|
-
changedChat() { changedSource(source); }, reply(event) { publishReply(source, event); }, detachChat() { discard(source); sync(); }, snapshot: () => snapshot(source), dispose };
|
|
299
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/** #1629: host cadence before ANY effect frame; never learns a baseline from an active effect. */
|
|
2
|
-
const finite = n => typeof n === "number" && Number.isFinite(n);
|
|
3
|
-
export function captureEffectBaseline({ window: host, empty, clock = () => host.performance.now() } = {}) {
|
|
4
|
-
let finish, settled = false, raf = null, timer = null, last = null, first = null, count = 0;
|
|
5
|
-
const promise = new Promise(resolve => { finish = resolve; });
|
|
6
|
-
const end = value => {
|
|
7
|
-
if (settled) return;
|
|
8
|
-
settled = true;
|
|
9
|
-
if (raf !== null) host.cancelAnimationFrame(raf);
|
|
10
|
-
if (timer !== null) host.clearTimeout(timer);
|
|
11
|
-
host.document.removeEventListener("visibilitychange", visibility);
|
|
12
|
-
finish(value);
|
|
13
|
-
};
|
|
14
|
-
const fail = reason => end({ ok: false, reason });
|
|
15
|
-
const gates = () => {
|
|
16
|
-
try { return host.document.visibilityState === "visible" && empty() === true; } catch { return false; }
|
|
17
|
-
};
|
|
18
|
-
const time = () => {
|
|
19
|
-
let now; try { now = clock(); } catch { return null; }
|
|
20
|
-
if (!finite(now) || now < 0 || last !== null && now < last) return null;
|
|
21
|
-
last = now; return now;
|
|
22
|
-
};
|
|
23
|
-
const visibility = () => { if (!gates()) fail("baseline_unavailable"); };
|
|
24
|
-
const step = () => {
|
|
25
|
-
if (settled) return;
|
|
26
|
-
raf = null;
|
|
27
|
-
if (!gates()) { fail("baseline_unavailable"); return; }
|
|
28
|
-
const now = time();
|
|
29
|
-
if (now === null) { fail("clock_unavailable"); return; }
|
|
30
|
-
if (now - began >= 10000) { fail("baseline_timeout"); return; }
|
|
31
|
-
if (first === null) { first = now; count = 1; }
|
|
32
|
-
else if (now > sample) count++;
|
|
33
|
-
sample = now;
|
|
34
|
-
if (now - first >= 2000) {
|
|
35
|
-
const fps = (count - 1) * 1000 / (now - first);
|
|
36
|
-
if (!finite(fps) || fps <= 0 || fps > 1000 || !gates()) fail("baseline_unavailable");
|
|
37
|
-
else end({ ok: true, baselineFps: fps });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
raf = host.requestAnimationFrame(step);
|
|
41
|
-
};
|
|
42
|
-
let began = null, sample = null;
|
|
43
|
-
if (!host || !host.document || typeof empty !== "function" || typeof clock !== "function" || ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout"].some(k => typeof host[k] !== "function") || typeof host.document.addEventListener !== "function" || typeof host.document.removeEventListener !== "function") {
|
|
44
|
-
settled = true; finish({ ok: false, reason: "baseline_unsupported" });
|
|
45
|
-
return { promise, cancel() {} };
|
|
46
|
-
}
|
|
47
|
-
began = time();
|
|
48
|
-
if (began === null) fail("clock_unavailable");
|
|
49
|
-
else if (!gates()) fail("baseline_unavailable");
|
|
50
|
-
else {
|
|
51
|
-
host.document.addEventListener("visibilitychange", visibility);
|
|
52
|
-
const deadline = () => {
|
|
53
|
-
if (settled) return;
|
|
54
|
-
if (!gates()) { fail("baseline_unavailable"); return; }
|
|
55
|
-
const now = time();
|
|
56
|
-
if (now === null) fail("clock_unavailable");
|
|
57
|
-
else if (now - began >= 10000) fail("baseline_timeout");
|
|
58
|
-
else timer = host.setTimeout(deadline, Math.max(1, 10000 - (now - began)));
|
|
59
|
-
};
|
|
60
|
-
timer = host.setTimeout(deadline, 10000);
|
|
61
|
-
raf = host.requestAnimationFrame(step);
|
|
62
|
-
}
|
|
63
|
-
return { promise, cancel() { fail("baseline_cancelled"); } };
|
|
64
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { chatEffectIdentity } from "./effect-display-metadata.js";
|
|
2
|
-
/** #1629: one committed local Chat lease joins the reviewed host-only effect primitives. */
|
|
3
|
-
import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
|
|
4
|
-
import { createEffectRegistry } from "./effect-registry.js";
|
|
5
|
-
import { createEffectSafety } from "./effect-safety.js";
|
|
6
|
-
import { stockEffectTrust } from "./effect-stock-trust.js";
|
|
7
|
-
import { createEffectProfileCollector } from "./effect-profiles.js";
|
|
8
|
-
|
|
9
|
-
const writers = new WeakMap();
|
|
10
|
-
/** Physical write reservations outlive a canceled caller. Responses never supply runtime authority. */
|
|
11
|
-
export function createEffectWriter(host, current) {
|
|
12
|
-
let dead = false;
|
|
13
|
-
const own = new Set();
|
|
14
|
-
if (!writers.has(host)) writers.set(host, new Set());
|
|
15
|
-
const all = writers.get(host);
|
|
16
|
-
let previous = null;
|
|
17
|
-
function stop() { if (dead) return; dead = true; for (const op of own) op.cancel(); }
|
|
18
|
-
function time() {
|
|
19
|
-
let now; try { now = host.performance.now(); } catch { stop(); return null; }
|
|
20
|
-
if (!Number.isFinite(now) || now < 0 || previous !== null && now < previous) { stop(); return null; }
|
|
21
|
-
previous = now; return now;
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
async post(path, body) {
|
|
25
|
-
if (dead || !current() || all.size >= 3) return false;
|
|
26
|
-
const began = time(); if (began === null) return false;
|
|
27
|
-
const text = JSON.stringify(body); if (new TextEncoder().encode(text).length > 16384) return false;
|
|
28
|
-
const controller = new host.AbortController(), op = { controller }; own.add(op); all.add(op);
|
|
29
|
-
let timer, cancel;
|
|
30
|
-
const aborted = new Promise(resolve => { cancel = () => { controller.abort(); resolve(false); }; }); op.cancel = cancel;
|
|
31
|
-
const work = Promise.resolve().then(async () => {
|
|
32
|
-
if (dead || !current() || controller.signal.aborted) return false;
|
|
33
|
-
const r = await host.fetch(path, { method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: text, signal: controller.signal });
|
|
34
|
-
// Settle the body before returning the physical reservation, even if the caller already timed out.
|
|
35
|
-
try { await r.body?.cancel(); } catch { /* no data is consumed */ }
|
|
36
|
-
const now = time();
|
|
37
|
-
return !dead && current() && !controller.signal.aborted && Number.isFinite(began) && began >= 0 && now !== null && now >= began && now - began < 10000 && r.ok && !r.redirected;
|
|
38
|
-
}).catch(() => false).finally(() => { host.clearTimeout(timer); controller.abort(); own.delete(op); all.delete(op); });
|
|
39
|
-
timer = host.setTimeout(cancel, 10000);
|
|
40
|
-
return Promise.race([work, aborted]);
|
|
41
|
-
},
|
|
42
|
-
dispose: stop,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Frozen, server-rendered session identity only. A header-token/public/View load has no identity. */
|
|
47
|
-
export { chatEffectIdentity } from "./effect-display-metadata.js";
|
|
48
|
-
/** Chat-only public/guest capability stays separate from personal preferences and management. */
|
|
49
|
-
export function effectPlaybackIdentity(host) {
|
|
50
|
-
try {
|
|
51
|
-
const b = host.document.body.dataset;
|
|
52
|
-
if (b.mode !== "full" || typeof b.effectEpoch !== "string" || !/^[a-f0-9]{16}$/.test(b.effectEpoch)) return null;
|
|
53
|
-
if (b.effectScope === "owner") {
|
|
54
|
-
const own = chatEffectIdentity(host);
|
|
55
|
-
return own?.epoch === b.effectEpoch ? Object.freeze({ epoch: b.effectEpoch, scope: "owner" }) : null;
|
|
56
|
-
}
|
|
57
|
-
if (b.effectScope === "chat" && (b.publicLink === "1" && b.webTransport === "poll" || b.tier === "guest")) return Object.freeze({ epoch: b.effectEpoch, scope: "chat" });
|
|
58
|
-
} catch { /* no session capability */ }
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
export function chatEffectState(state, name) {
|
|
62
|
-
const inst = state.instances?.find(i => i.name === name);
|
|
63
|
-
if (!inst) return null;
|
|
64
|
-
if (["stopped", "paused"].includes(inst.status)) return "stopped";
|
|
65
|
-
if (Object.hasOwn(state.awaiting ?? {}, name)) return "needs_you";
|
|
66
|
-
return ["working", "stuck"].includes(state.exec?.[name]) ? "working" : state.exec?.[name] === "idle" ? "idle" : null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** `observe` supplies closed host gates, never event/message content. No timers poll server state. */
|
|
70
|
-
export function createChatEffectRuntime({ window: host, name, element, current, observe, onChange = () => {}, factories = {} }) {
|
|
71
|
-
let dead = false, error = null, loader = null, registry = null, writer = null, active = null, queued = false, generation = 0, metadata = null, ctx = null, lastContext = null;
|
|
72
|
-
const paused = new Map(), owners = new Map();
|
|
73
|
-
const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
|
|
74
|
-
const identity = chatEffectIdentity(host), auth = host.AgendAuth;
|
|
75
|
-
const alive = () => !dead && current() === true && element.isConnected === true && !auth.sessionEnded();
|
|
76
|
-
const tell = () => { if (!dead && current()) onChange({ error, rows: registry?.snapshot() ?? [], effects: metadata?.effects ?? [], loading: metadata === null && !error }); };
|
|
77
|
-
const fail = reason => { if (dead) return; error = reason; generation++; metadata = null; active?.controller.abort(); registry?.dispose?.(); registry = null; loader?.dispose?.(); loader = null; writer?.dispose(); tell(); };
|
|
78
|
-
function dispose() {
|
|
79
|
-
if (dead) return; dead = true; generation++; active?.controller.abort(); registry?.dispose?.(); loader?.dispose?.(); writer?.dispose(); offAuth?.();
|
|
80
|
-
for (const value of owners.values()) value.collector.dispose(); owners.clear();
|
|
81
|
-
}
|
|
82
|
-
let offAuth = null;
|
|
83
|
-
if (!identity || typeof auth?.onSessionEnd !== "function" || typeof auth?.sessionEnded !== "function" || typeof name !== "string" || name.length < 1 || name.length > 128 || !current()) return { ok: false, dispose };
|
|
84
|
-
offAuth = auth.onSessionEnd(dispose); // May synchronously replay an already-ended episode.
|
|
85
|
-
if (dead || !alive()) { dispose(); return { ok: false, dispose }; }
|
|
86
|
-
const clock = () => host.performance.now();
|
|
87
|
-
try {
|
|
88
|
-
const make = (key, fn) => factories[key] ?? fn;
|
|
89
|
-
writer = createEffectWriter(host, alive);
|
|
90
|
-
loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive });
|
|
91
|
-
if (!loader.ok) throw Error("runtime_unavailable");
|
|
92
|
-
registry = make("registry", createEffectRegistry)({ window: host, origin: host.location?.origin, boot: host.document.body.dataset.effectBoot,
|
|
93
|
-
load: async (d, signal) => {
|
|
94
|
-
const mine = generation, value = await loader.resource({ id: d.id, instance: name }, signal);
|
|
95
|
-
if (!alive() || mine !== generation || signal.aborted) return null;
|
|
96
|
-
if (!value) { fail("resource_unavailable"); return null; }
|
|
97
|
-
return value;
|
|
98
|
-
},
|
|
99
|
-
safety: make("safety", createEffectSafety)({ storage: host.localStorage, crypto: host.crypto }),
|
|
100
|
-
stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
|
|
101
|
-
onChange() { tell(); emit(); },
|
|
102
|
-
onMount(owner) {
|
|
103
|
-
if (!alive() || !owner.current()) return;
|
|
104
|
-
const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
|
|
105
|
-
if (collector.ok) owners.set(owner, { collector, began: clock(), attempted: null });
|
|
106
|
-
},
|
|
107
|
-
onRetire(owner) { const row = owners.get(owner); row?.collector.dispose(); owners.delete(owner); },
|
|
108
|
-
onProfile(raw, owner) {
|
|
109
|
-
const row = owners.get(owner); if (!row || !alive() || !owner.current()) return;
|
|
110
|
-
row.collector.add(raw);
|
|
111
|
-
const now = clock();
|
|
112
|
-
if (now - row.began < 10000 && raw.verdict !== "paused" || row.attempted !== null && now - row.attempted < 10000) return;
|
|
113
|
-
const report = row.collector.report(); if (!report || !owner.current()) return;
|
|
114
|
-
row.attempted = now;
|
|
115
|
-
void writer.post(`/ui/effects/profile/${owner.id}?instance=${encodeURIComponent(name)}&ownerEpoch=${identity.epoch}`, report.profile);
|
|
116
|
-
}, clock,
|
|
117
|
-
});
|
|
118
|
-
if (!registry.ok) throw Error("runtime_unavailable");
|
|
119
|
-
} catch { fail("runtime_unavailable"); }
|
|
120
|
-
function descriptors() {
|
|
121
|
-
return (metadata?.effects ?? []).filter(p => p.manifest.regions.includes("chat")).map((p, order) => ({ key: `chat:${p.id}`, id: p.id, by: p.by, region: "chat", layer: p.manifest.layer,
|
|
122
|
-
order, paused: paused.get(p.id) === placementIdentity(p), visible: true, instance: name, params: p.params, element }));
|
|
123
|
-
}
|
|
124
|
-
function emit() {
|
|
125
|
-
if (!alive() || error || !registry?.ok) return;
|
|
126
|
-
const value = observe();
|
|
127
|
-
if (["idle", "working", "needs_you", "stopped"].includes(value.state)) registry.publish("state", { state: value.state }, name);
|
|
128
|
-
if (["light", "dark"].includes(value.mode)) registry.publish("theme", { mode: value.mode });
|
|
129
|
-
}
|
|
130
|
-
function sync() {
|
|
131
|
-
if (!alive()) { dispose(); return; } if (error) return;
|
|
132
|
-
const value = observe(); ctx = value.context;
|
|
133
|
-
const stamp = JSON.stringify(ctx);
|
|
134
|
-
if (stamp !== lastContext) { lastContext = stamp; registry.update(descriptors(), ctx); }
|
|
135
|
-
emit();
|
|
136
|
-
if (!metadata && !active && ctx.enabled && ctx.show && ctx.transportReady && !ctx.safe && !ctx.publicLink && !ctx.reducedMotion && !ctx.hidden) void read();
|
|
137
|
-
}
|
|
138
|
-
async function read() {
|
|
139
|
-
if (!alive() || error || active) return;
|
|
140
|
-
const op = { controller: new host.AbortController(), generation }; active = op;
|
|
141
|
-
try {
|
|
142
|
-
const answer = await loader.placements(name, op.controller.signal);
|
|
143
|
-
if (!alive() || active !== op) return;
|
|
144
|
-
if (op.generation !== generation) return;
|
|
145
|
-
if (!answer) { fail("resource_unavailable"); return; }
|
|
146
|
-
metadata = answer;
|
|
147
|
-
for (const [id, stamp] of paused) if (!answer.effects.some(p => p.id === id && placementIdentity(p) === stamp)) paused.delete(id);
|
|
148
|
-
lastContext = null; sync(); tell();
|
|
149
|
-
} finally {
|
|
150
|
-
if (active === op) active = null;
|
|
151
|
-
if (queued && alive() && !error) { queued = false; sync(); }
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { ok: !error && !dead, sync,
|
|
155
|
-
changed() { if (!alive() || error) return; generation++; metadata = null; registry.update([], observe().context); lastContext = null; if (active) queued = true; else sync(); tell(); },
|
|
156
|
-
reply(event) { if (alive() && !error && event.instance === name) registry.publish("reply", {}, name); },
|
|
157
|
-
pause(id) { if (!alive() || !metadata?.effects.some(p => p.id === id)) return false; paused.set(id, placementIdentity(metadata.effects.find(p => p.id === id))); lastContext = null; sync(); return true; },
|
|
158
|
-
resume(id) { if (!alive() || error || !metadata?.effects.some(p => p.id === id)) return false; paused.delete(id); lastContext = null; sync(); return registry.resume(`chat:${id}`); },
|
|
159
|
-
async remove(id) { if (!alive() || error || !metadata?.effects.some(p => p.id === id)) return false; const mine = generation; const ok = await writer.post("/ui/effects/remove", { instance: name, id }); if (!alive() || mine !== generation) return false; if (ok) this.changed(); return ok; },
|
|
160
|
-
snapshot: () => ({ error, rows: registry?.snapshot() ?? [], effects: metadata?.effects ?? [], loading: metadata === null && !error }), dispose,
|
|
161
|
-
};
|
|
162
|
-
}
|