beehaven 0.8.16
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 +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentSessions — the daemon holds one live WebSocket per active AgentIdentity
|
|
3
|
+
* to the cloud workspace, so `beehaven agent login <id>` (a cloud agent) shows up
|
|
4
|
+
* as a real `role:'agent'` session in the workspace (visible in my-devices /
|
|
5
|
+
* Sessions). These are minimal keepalive sockets: the cloud DO does the auth
|
|
6
|
+
* (the agent token in the handshake → role:'agent') and all session bookkeeping;
|
|
7
|
+
* the daemon just keeps the socket open until the agent disconnects.
|
|
8
|
+
*
|
|
9
|
+
* The token is a CLOUD-signed AgentIdentity (minted by the worker's
|
|
10
|
+
* `agent-mint-token` RPC), so it verifies against the cloud's AGENT_IDENTITY_SECRET
|
|
11
|
+
* without any daemon↔cloud secret sync.
|
|
12
|
+
*/
|
|
13
|
+
import { writeFileSync, readFileSync, renameSync, chmodSync, mkdirSync, existsSync } from 'node:fs';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
18
|
+
function scopedPresenceFile(scope) {
|
|
19
|
+
const digest = createHash('sha256').update(String(scope || 'local')).digest('hex').slice(0, 24);
|
|
20
|
+
return join(CONFIG_DIR, `agent-presence-${digest}.json`);
|
|
21
|
+
}
|
|
22
|
+
export class AgentSessions {
|
|
23
|
+
wsBase;
|
|
24
|
+
conns = new Map();
|
|
25
|
+
/** Connection intent survives an unexpected relay close so the socket can be rebuilt. */
|
|
26
|
+
specs = new Map();
|
|
27
|
+
lastInteractionAt = new Map();
|
|
28
|
+
/** One deadline per identity. Presence expiry is scheduled from activity, not swept by polling. */
|
|
29
|
+
idleTimers = new Map();
|
|
30
|
+
presenceFile;
|
|
31
|
+
presenceBroadcast = null;
|
|
32
|
+
/** A desktop identity is presence, not a permanent login badge. */
|
|
33
|
+
static IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
34
|
+
static RECONNECT_MIN_MS = 1_000;
|
|
35
|
+
static RECONNECT_MAX_MS = 30_000;
|
|
36
|
+
/** @param wsBase returns the relay ws base, e.g. wss://…/ (no trailing path) */
|
|
37
|
+
constructor(wsBase, presenceScope = 'local') {
|
|
38
|
+
this.wsBase = wsBase;
|
|
39
|
+
// The recovery clock is account + environment scoped. A global file let a reused agent handle
|
|
40
|
+
// carry a recent lease from one Clearly account (or staging) into another after a daemon restart.
|
|
41
|
+
this.presenceFile = scopedPresenceFile(presenceScope);
|
|
42
|
+
// A daemon restart ends every in-memory socket. The active projection is cleared below, but
|
|
43
|
+
// the interaction clock is durable so a recent agent session can be reattached instead of
|
|
44
|
+
// falling back to the much older login timestamp.
|
|
45
|
+
this.loadPersistedInteractions();
|
|
46
|
+
this.syncActiveSessionsFile();
|
|
47
|
+
}
|
|
48
|
+
/** Attach the local IPC fan-out after the daemon server exists. The data itself is sourced from
|
|
49
|
+
* the authenticated cloud-session sockets; the file below remains only crash/restart recovery
|
|
50
|
+
* state and is not a UI subscription surface. */
|
|
51
|
+
setPresenceBroadcast(fn) {
|
|
52
|
+
this.presenceBroadcast = fn;
|
|
53
|
+
this.broadcastPresence();
|
|
54
|
+
}
|
|
55
|
+
snapshot() {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
// A transport is not activity. Only identities with a real, attributed interaction inside
|
|
58
|
+
// the lease belong in the live projection; login, reconnect and daemon restore must not wake
|
|
59
|
+
// an avatar by themselves.
|
|
60
|
+
const activeAgentIds = [...this.specs.keys()].filter((agentId) => {
|
|
61
|
+
const at = this.lastInteractionAt.get(agentId) ?? 0;
|
|
62
|
+
return Number.isFinite(at) && at > 0 && now - at <= AgentSessions.IDLE_TIMEOUT_MS;
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
activeAgentIds,
|
|
66
|
+
lastInteractionAt: Object.fromEntries(this.lastInteractionAt.entries()),
|
|
67
|
+
daemonPid: process.pid,
|
|
68
|
+
updatedAt: now,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
broadcastPresence() {
|
|
72
|
+
try {
|
|
73
|
+
this.presenceBroadcast?.(this.snapshot());
|
|
74
|
+
}
|
|
75
|
+
catch { /* IPC fan-out is best-effort */ }
|
|
76
|
+
}
|
|
77
|
+
loadPersistedInteractions() {
|
|
78
|
+
try {
|
|
79
|
+
if (!existsSync(this.presenceFile))
|
|
80
|
+
return;
|
|
81
|
+
const raw = JSON.parse(readFileSync(this.presenceFile, 'utf8'));
|
|
82
|
+
let pruned = false;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
for (const [agentId, value] of Object.entries(raw || {})) {
|
|
85
|
+
const at = Number(value);
|
|
86
|
+
if (!agentId || !Number.isFinite(at) || at <= 0) {
|
|
87
|
+
pruned = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (now - at > AgentSessions.IDLE_TIMEOUT_MS) {
|
|
91
|
+
pruned = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
this.lastInteractionAt.set(agentId, at);
|
|
95
|
+
}
|
|
96
|
+
if (pruned)
|
|
97
|
+
this.syncPersistedInteractions();
|
|
98
|
+
}
|
|
99
|
+
catch { /* best-effort; a missing/corrupt clock must not stop the daemon */ }
|
|
100
|
+
}
|
|
101
|
+
syncPersistedInteractions() {
|
|
102
|
+
try {
|
|
103
|
+
const dir = CONFIG_DIR;
|
|
104
|
+
if (!existsSync(dir))
|
|
105
|
+
mkdirSync(dir, { recursive: true });
|
|
106
|
+
const tmp = `${this.presenceFile}.tmp`;
|
|
107
|
+
writeFileSync(tmp, JSON.stringify(Object.fromEntries(this.lastInteractionAt.entries()), null, 2), { mode: 0o600 });
|
|
108
|
+
renameSync(tmp, this.presenceFile);
|
|
109
|
+
try {
|
|
110
|
+
chmodSync(this.presenceFile, 0o600);
|
|
111
|
+
}
|
|
112
|
+
catch { /* best effort */ }
|
|
113
|
+
}
|
|
114
|
+
catch { /* best-effort; presence remains correct in memory */ }
|
|
115
|
+
}
|
|
116
|
+
syncActiveSessionsFile() {
|
|
117
|
+
const snapshot = this.snapshot();
|
|
118
|
+
this.broadcastPresence();
|
|
119
|
+
try {
|
|
120
|
+
const dir = join(homedir(), '.beehaven');
|
|
121
|
+
if (!existsSync(dir))
|
|
122
|
+
mkdirSync(dir, { recursive: true });
|
|
123
|
+
writeFileSync(join(dir, 'active-agent-sessions.json'), JSON.stringify({
|
|
124
|
+
updatedAt: snapshot.updatedAt,
|
|
125
|
+
daemonPid: snapshot.daemonPid,
|
|
126
|
+
// A transport reconnect is not an explicit agent logout. Keep presence projected while
|
|
127
|
+
// the socket is being rebuilt so the desktop does not remove avatars on a brief relay
|
|
128
|
+
// drop.
|
|
129
|
+
activeAgentIds: snapshot.activeAgentIds,
|
|
130
|
+
lastInteractionAt: snapshot.lastInteractionAt,
|
|
131
|
+
}, null, 2), 'utf8');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* best-effort session sync */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Agent ids with a live or reconnecting session. */
|
|
138
|
+
list() {
|
|
139
|
+
return [...this.specs.keys()];
|
|
140
|
+
}
|
|
141
|
+
/** Reattach a verified identity after the daemon was replaced, without making login the only
|
|
142
|
+
* event that can restart its local presence session. `connect` sets the spec synchronously. */
|
|
143
|
+
ensureConnected(agentId, token) {
|
|
144
|
+
const id = String(agentId || '').trim();
|
|
145
|
+
const tok = String(token || '').trim();
|
|
146
|
+
if (!id || !tok || this.specs.has(id))
|
|
147
|
+
return;
|
|
148
|
+
void this.connect(id, tok).catch(() => undefined);
|
|
149
|
+
}
|
|
150
|
+
/** Touch the matching live identity after a verified interaction. */
|
|
151
|
+
touch(agentId, at = Date.now()) {
|
|
152
|
+
const id = String(agentId || '').trim();
|
|
153
|
+
if (!id || !this.specs.has(id) || !Number.isFinite(at))
|
|
154
|
+
return;
|
|
155
|
+
const previous = this.lastInteractionAt.get(id) ?? 0;
|
|
156
|
+
if (at <= previous)
|
|
157
|
+
return;
|
|
158
|
+
this.lastInteractionAt.set(id, at);
|
|
159
|
+
this.scheduleIdleExpiry(id);
|
|
160
|
+
this.syncPersistedInteractions();
|
|
161
|
+
this.syncActiveSessionsFile();
|
|
162
|
+
}
|
|
163
|
+
clearIdleExpiry(agentId) {
|
|
164
|
+
const timer = this.idleTimers.get(agentId);
|
|
165
|
+
if (timer)
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
this.idleTimers.delete(agentId);
|
|
168
|
+
}
|
|
169
|
+
/** Schedule the exact end of this identity's 30-minute presence lease. A newer touch replaces
|
|
170
|
+
* the deadline; a stale callback cannot disconnect a session that became active again. */
|
|
171
|
+
scheduleIdleExpiry(agentId) {
|
|
172
|
+
this.clearIdleExpiry(agentId);
|
|
173
|
+
const last = this.lastInteractionAt.get(agentId);
|
|
174
|
+
if (!last || !this.specs.has(agentId))
|
|
175
|
+
return;
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
this.idleTimers.delete(agentId);
|
|
178
|
+
if (this.lastInteractionAt.get(agentId) !== last) {
|
|
179
|
+
this.scheduleIdleExpiry(agentId);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (this.specs.has(agentId))
|
|
183
|
+
this.disconnect(agentId);
|
|
184
|
+
}, Math.max(0, last + AgentSessions.IDLE_TIMEOUT_MS - Date.now()));
|
|
185
|
+
timer.unref?.();
|
|
186
|
+
this.idleTimers.set(agentId, timer);
|
|
187
|
+
}
|
|
188
|
+
scheduleReconnect(agentId, spec) {
|
|
189
|
+
if (this.specs.get(agentId) !== spec || spec.reconnectTimer)
|
|
190
|
+
return;
|
|
191
|
+
const delay = spec.reconnectDelay;
|
|
192
|
+
spec.reconnectDelay = Math.min(spec.reconnectDelay * 2, AgentSessions.RECONNECT_MAX_MS);
|
|
193
|
+
spec.reconnectTimer = setTimeout(() => {
|
|
194
|
+
spec.reconnectTimer = null;
|
|
195
|
+
this.openSocket(agentId, spec);
|
|
196
|
+
}, delay);
|
|
197
|
+
spec.reconnectTimer.unref?.();
|
|
198
|
+
}
|
|
199
|
+
/** Open one transport. Unexpected relay/network closes retain local presence and reconnect;
|
|
200
|
+
* explicit logout, idle expiry, clean close, and auth/session rejection remove it. */
|
|
201
|
+
openSocket(agentId, spec, firstResolve) {
|
|
202
|
+
const WS = globalThis.WebSocket;
|
|
203
|
+
if (!WS) {
|
|
204
|
+
firstResolve?.({ ok: false, error: 'WebSocket unavailable in this runtime' });
|
|
205
|
+
this.specs.delete(agentId);
|
|
206
|
+
this.lastInteractionAt.delete(agentId);
|
|
207
|
+
this.clearIdleExpiry(agentId);
|
|
208
|
+
this.syncPersistedInteractions();
|
|
209
|
+
this.syncActiveSessionsFile();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const base = this.wsBase().replace(/\/$/, '');
|
|
213
|
+
const url = `${base}/my/hive/ws?token=${encodeURIComponent(spec.token)}&clientType=agent&client=agent&wsType=workspace&deviceId=agent-${encodeURIComponent(agentId)}`;
|
|
214
|
+
let ws;
|
|
215
|
+
let opened = false;
|
|
216
|
+
let settled = false;
|
|
217
|
+
let timer = null;
|
|
218
|
+
const settleFirst = (result) => {
|
|
219
|
+
if (!firstResolve || settled)
|
|
220
|
+
return;
|
|
221
|
+
settled = true;
|
|
222
|
+
if (timer)
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
firstResolve(result);
|
|
225
|
+
};
|
|
226
|
+
try {
|
|
227
|
+
ws = new WS(url);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
settleFirst({ ok: false, error: e?.message ?? String(e) });
|
|
231
|
+
this.scheduleReconnect(agentId, spec);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (firstResolve) {
|
|
235
|
+
timer = setTimeout(() => {
|
|
236
|
+
settleFirst({ ok: false, error: 'connect timeout' });
|
|
237
|
+
try {
|
|
238
|
+
ws?.close();
|
|
239
|
+
}
|
|
240
|
+
catch { /* noop */ }
|
|
241
|
+
}, 10_000);
|
|
242
|
+
}
|
|
243
|
+
ws.onopen = () => {
|
|
244
|
+
if (this.specs.get(agentId) !== spec) {
|
|
245
|
+
try {
|
|
246
|
+
ws.close();
|
|
247
|
+
}
|
|
248
|
+
catch { /* noop */ }
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
opened = true;
|
|
252
|
+
this.conns.set(agentId, ws);
|
|
253
|
+
// Reconnecting transport is not agent activity. A new socket stays inactive until a
|
|
254
|
+
// verified call touches the identity; a persisted interaction keeps its existing clock.
|
|
255
|
+
this.scheduleIdleExpiry(agentId);
|
|
256
|
+
spec.reconnectDelay = AgentSessions.RECONNECT_MIN_MS;
|
|
257
|
+
this.syncPersistedInteractions();
|
|
258
|
+
this.syncActiveSessionsFile();
|
|
259
|
+
settleFirst({ ok: true });
|
|
260
|
+
};
|
|
261
|
+
ws.onerror = (e) => {
|
|
262
|
+
if (!opened)
|
|
263
|
+
settleFirst({ ok: false, error: e?.message ?? e?.error?.message ?? 'ws error' });
|
|
264
|
+
};
|
|
265
|
+
ws.onclose = (event) => {
|
|
266
|
+
if (timer)
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
if (this.specs.get(agentId) !== spec)
|
|
269
|
+
return;
|
|
270
|
+
if (this.conns.get(agentId) === ws)
|
|
271
|
+
this.conns.delete(agentId);
|
|
272
|
+
if (!opened) {
|
|
273
|
+
settleFirst({ ok: false, error: 'connection closed before open' });
|
|
274
|
+
this.specs.delete(agentId);
|
|
275
|
+
this.lastInteractionAt.delete(agentId);
|
|
276
|
+
this.clearIdleExpiry(agentId);
|
|
277
|
+
this.syncPersistedInteractions();
|
|
278
|
+
this.syncActiveSessionsFile();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const code = Number(event?.code) || 0;
|
|
282
|
+
// 1006/1011 are transport/workspace failures. A clean close or auth/session close is
|
|
283
|
+
// authoritative and must not resurrect an explicitly ended session.
|
|
284
|
+
if (code === 1000 || (code >= 4000 && code < 5000)) {
|
|
285
|
+
this.specs.delete(agentId);
|
|
286
|
+
this.lastInteractionAt.delete(agentId);
|
|
287
|
+
this.clearIdleExpiry(agentId);
|
|
288
|
+
this.syncPersistedInteractions();
|
|
289
|
+
this.syncActiveSessionsFile();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.syncActiveSessionsFile();
|
|
293
|
+
this.scheduleReconnect(agentId, spec);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Open (or replace) the agent's workspace session. Resolves once the initial socket is OPEN
|
|
297
|
+
* (the DO has created the session) or on initial error/timeout. */
|
|
298
|
+
async connect(agentId, token, preserveInteractionAt = false) {
|
|
299
|
+
this.disconnect(agentId, preserveInteractionAt);
|
|
300
|
+
const spec = {
|
|
301
|
+
token,
|
|
302
|
+
reconnectTimer: null,
|
|
303
|
+
reconnectDelay: AgentSessions.RECONNECT_MIN_MS,
|
|
304
|
+
};
|
|
305
|
+
this.specs.set(agentId, spec);
|
|
306
|
+
return new Promise((resolve) => this.openSocket(agentId, spec, resolve));
|
|
307
|
+
}
|
|
308
|
+
/** Rehydrate identities that were intentionally left logged in when the daemon process died or
|
|
309
|
+
* was replaced. Explicit logout removes `sessionId` from the stored record, so this does not
|
|
310
|
+
* resurrect an identity the user deliberately signed out of. */
|
|
311
|
+
async restorePersistedSessions(records) {
|
|
312
|
+
await Promise.all(records.map(async (record) => {
|
|
313
|
+
const agentId = String(record.agentId || '').trim();
|
|
314
|
+
const token = String(record.token || '').trim();
|
|
315
|
+
if (!agentId || !token || !record.sessionId)
|
|
316
|
+
return;
|
|
317
|
+
if (typeof record.expiresAt === 'number' && record.expiresAt <= Date.now())
|
|
318
|
+
return;
|
|
319
|
+
// A session id can outlive the daemon by days if an older client crashed. Do not resurrect
|
|
320
|
+
// those stale handles on every app launch; use the last VERIFIED interaction, not login age,
|
|
321
|
+
// because an agent can keep working after it first logged in. Thirty minutes matches
|
|
322
|
+
// the desktop presence lease.
|
|
323
|
+
const lastInteractionAt = Number(record.lastInteractionAt || this.lastInteractionAt.get(agentId));
|
|
324
|
+
if (!Number.isFinite(lastInteractionAt)
|
|
325
|
+
|| Date.now() - lastInteractionAt >= AgentSessions.IDLE_TIMEOUT_MS)
|
|
326
|
+
return;
|
|
327
|
+
if (this.specs.has(agentId))
|
|
328
|
+
return;
|
|
329
|
+
await this.connect(agentId, token, true).catch(() => undefined);
|
|
330
|
+
if (this.specs.has(agentId) && Number.isFinite(lastInteractionAt) && lastInteractionAt > 0) {
|
|
331
|
+
// A real call may arrive while the replacement socket is opening. Never let a late
|
|
332
|
+
// restore callback move that newer interaction backwards to the old login/session time.
|
|
333
|
+
const current = this.lastInteractionAt.get(agentId) ?? 0;
|
|
334
|
+
if (current < lastInteractionAt)
|
|
335
|
+
this.lastInteractionAt.set(agentId, lastInteractionAt);
|
|
336
|
+
this.scheduleIdleExpiry(agentId);
|
|
337
|
+
this.syncPersistedInteractions();
|
|
338
|
+
this.syncActiveSessionsFile();
|
|
339
|
+
}
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
/** Close the agent's session (idempotent). */
|
|
343
|
+
disconnect(agentId, preserveInteractionAt = false) {
|
|
344
|
+
this.clearIdleExpiry(agentId);
|
|
345
|
+
const spec = this.specs.get(agentId);
|
|
346
|
+
if (spec?.reconnectTimer) {
|
|
347
|
+
clearTimeout(spec.reconnectTimer);
|
|
348
|
+
spec.reconnectTimer = null;
|
|
349
|
+
}
|
|
350
|
+
this.specs.delete(agentId);
|
|
351
|
+
const ws = this.conns.get(agentId);
|
|
352
|
+
if (ws) {
|
|
353
|
+
try {
|
|
354
|
+
ws.close();
|
|
355
|
+
}
|
|
356
|
+
catch { /* noop */ }
|
|
357
|
+
}
|
|
358
|
+
this.conns.delete(agentId);
|
|
359
|
+
if (!preserveInteractionAt) {
|
|
360
|
+
this.lastInteractionAt.delete(agentId);
|
|
361
|
+
this.syncPersistedInteractions();
|
|
362
|
+
}
|
|
363
|
+
this.syncActiveSessionsFile();
|
|
364
|
+
}
|
|
365
|
+
disconnectAll(preserveInteractionAt = false) {
|
|
366
|
+
for (const id of [...this.specs.keys()])
|
|
367
|
+
this.disconnect(id, preserveInteractionAt);
|
|
368
|
+
this.syncActiveSessionsFile();
|
|
369
|
+
}
|
|
370
|
+
}
|