@vibesdotdev/session 1.0.0-rc.1 → 1.0.0-rc.2
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/admission/admit-caller.context.descriptor.d.ts +33 -0
- package/dist/admission/admit-caller.context.descriptor.d.ts.map +1 -0
- package/dist/admission/admit-caller.context.descriptor.js +10 -0
- package/dist/admission/admit-caller.context.descriptor.js.map +1 -0
- package/dist/admission/admit-caller.context.impl.d.ts +8 -0
- package/dist/admission/admit-caller.context.impl.d.ts.map +1 -0
- package/dist/admission/admit-caller.context.impl.js +15 -0
- package/dist/admission/admit-caller.context.impl.js.map +1 -0
- package/dist/admission/admit-caller.d.ts +57 -0
- package/dist/admission/admit-caller.d.ts.map +1 -0
- package/dist/admission/admit-caller.js +163 -0
- package/dist/admission/admit-caller.js.map +1 -0
- package/dist/cli/services/session/sessions.d.ts +4 -4
- package/dist/cli/services/session.cli.consumer.d.ts +1 -1
- package/dist/manager.d.ts +87 -2
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +363 -3
- package/dist/manager.js.map +1 -1
- package/dist/mcp/schemas/tools.d.ts +3 -0
- package/dist/mcp/schemas/tools.d.ts.map +1 -1
- package/dist/mcp/schemas/tools.js +15 -2
- package/dist/mcp/schemas/tools.js.map +1 -1
- package/dist/mcp/utils/participant.d.ts +25 -8
- package/dist/mcp/utils/participant.d.ts.map +1 -1
- package/dist/mcp/utils/participant.js +26 -35
- package/dist/mcp/utils/participant.js.map +1 -1
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/participant.d.ts +30 -0
- package/dist/schemas/participant.d.ts.map +1 -1
- package/dist/schemas/participant.js +38 -1
- package/dist/schemas/participant.js.map +1 -1
- package/dist/schemas/session.d.ts +5 -5
- package/dist/schemas/session.js +1 -1
- package/dist/schemas/session.js.map +1 -1
- package/dist/session.plugin.d.ts.map +1 -1
- package/dist/session.plugin.js +3 -0
- package/dist/session.plugin.js.map +1 -1
- package/dist/storage/session-connections.record.descriptor.d.ts +9 -0
- package/dist/storage/session-connections.record.descriptor.d.ts.map +1 -1
- package/dist/storage/session-connections.record.descriptor.js +23 -2
- package/dist/storage/session-connections.record.descriptor.js.map +1 -1
- package/dist/storage/session-sessions.record.descriptor.d.ts +1 -1
- package/dist/tools/lifecycle/context/handler.d.ts +1 -1
- package/dist/tools/lifecycle/init/handler.d.ts.map +1 -1
- package/dist/tools/lifecycle/init/handler.js +16 -0
- package/dist/tools/lifecycle/init/handler.js.map +1 -1
- package/dist/tools/lifecycle/init/schemas.d.ts +7 -0
- package/dist/tools/lifecycle/init/schemas.d.ts.map +1 -1
- package/dist/tools/lifecycle/init/schemas.js +14 -1
- package/dist/tools/lifecycle/init/schemas.js.map +1 -1
- package/dist/tools/lifecycle/init/types.d.ts +6 -0
- package/dist/tools/lifecycle/init/types.d.ts.map +1 -1
- package/dist/tools/lifecycle/init/types.js.map +1 -1
- package/dist/tools/lifecycle/packet/handler.d.ts +2 -2
- package/dist/tools/lifecycle/snapshot/handler.d.ts +1 -1
- package/dist/tools/lifecycle/state/status/handler.d.ts +1 -1
- package/dist/tools/lifecycle/tool/schemas/index.d.ts +3 -0
- package/dist/tools/lifecycle/tool/schemas/index.d.ts.map +1 -1
- package/dist/tools/lifecycle/tool/session.tool.descriptor.js +5 -5
- package/dist/tools/lifecycle/tool/session.tool.descriptor.js.map +1 -1
- package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.d.ts +2 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.d.ts.map +1 -1
- package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.d.ts +2 -0
- package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.d.ts.map +1 -1
- package/dist/tools/lifecycle/tool/session.variant.update.impl.d.ts +1 -0
- package/dist/tools/lifecycle/tool/session.variant.update.impl.d.ts.map +1 -1
- package/dist/tools/lifecycle/update/handler.d.ts +2 -2
- package/dist/tools/lifecycle/update/handler.d.ts.map +1 -1
- package/dist/tools/lifecycle/update/handler.js +13 -0
- package/dist/tools/lifecycle/update/handler.js.map +1 -1
- package/dist/tools/lifecycle/update/schemas.d.ts +1 -0
- package/dist/tools/lifecycle/update/schemas.d.ts.map +1 -1
- package/dist/tools/lifecycle/update/schemas.js +3 -1
- package/dist/tools/lifecycle/update/schemas.js.map +1 -1
- package/dist/tools/messages/handlers.d.ts.map +1 -1
- package/dist/tools/messages/handlers.js +4 -2
- package/dist/tools/messages/handlers.js.map +1 -1
- package/dist/tools/messages/tool/session-collaborate.descriptor.js +1 -1
- package/dist/tools/messages/tool/session-collaborate.descriptor.js.map +1 -1
- package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.js +4 -4
- package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.js.map +1 -1
- package/dist/tools/todos/tool/session-todo.descriptor.js +3 -3
- package/dist/tools/todos/tool/session-todo.descriptor.js.map +1 -1
- package/dist/tools/workspace/tool/session-workspace.descriptor.js +1 -1
- package/dist/tools/workspace/tool/session-workspace.descriptor.js.map +1 -1
- package/package.json +11 -11
package/dist/manager.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as z from 'zod/v4';
|
|
15
15
|
import { sha256Hex } from '@vibesdotdev/shared/hashing';
|
|
16
|
-
import { CapabilityError, CaughtFailureSchema, valueFreeFailure } from '@vibesdotdev/kernel';
|
|
16
|
+
import { CapabilityError, CaughtFailureSchema, settle, valueFreeFailure } from '@vibesdotdev/kernel';
|
|
17
17
|
import { getVibesClient } from '@vibesdotdev/kernel/client';
|
|
18
18
|
import { JsonValueSchema as JsonSchema } from '@vibesdotdev/kernel/schemas/json';
|
|
19
19
|
import { getLogger } from '@vibesdotdev/logging';
|
|
@@ -21,7 +21,7 @@ import { StorageRecord } from '@vibesdotdev/storage/record';
|
|
|
21
21
|
import { storageFailureCondition } from '@vibesdotdev/storage/record/failure-condition';
|
|
22
22
|
import { ScratchpadContentTypeSchema } from "./schemas/index.js";
|
|
23
23
|
import { HarnessSeatRegistrationSchema, HarnessSeatSchema, harnessSeatParticipantId, harnessSeatSlug } from "./schemas/harness-seat.js";
|
|
24
|
-
import { ensureSessionContextWithManager, getOrCreateMCPParticipant, resolveParticipantReference } from "./mcp/utils/participant.js";
|
|
24
|
+
import { ensureSessionContextWithManager, getOrCreateMCPParticipant, resolveParticipantReference, sanitizeSlugCandidate, scanParticipantsBySlug } from "./mcp/utils/participant.js";
|
|
25
25
|
import { emitSessionBound } from "./events/session.bound.event.descriptor.js";
|
|
26
26
|
const SESSION_STATE_LEGACY_KEY = 'session-state';
|
|
27
27
|
const SESSION_STATE_DEFAULT_KEY = 'default';
|
|
@@ -510,6 +510,10 @@ export class SessionManager {
|
|
|
510
510
|
}
|
|
511
511
|
async close() {
|
|
512
512
|
await this.persistMappingsTail;
|
|
513
|
+
// Instances this host kept open stop heartbeating and expire on their TTL.
|
|
514
|
+
// Closing writes nothing: storage refuses new work while its runtime ends.
|
|
515
|
+
this.stopInstanceHeartbeat();
|
|
516
|
+
this.hostedInstances.clear();
|
|
513
517
|
// Storage descriptors handle their own cleanup
|
|
514
518
|
this.initialized = false;
|
|
515
519
|
this.participantSessions.clear();
|
|
@@ -596,11 +600,45 @@ export class SessionManager {
|
|
|
596
600
|
}
|
|
597
601
|
async updateSession(id, updates) {
|
|
598
602
|
await this.ensureStorage();
|
|
603
|
+
const { parentSessionId, ...rest } = updates;
|
|
604
|
+
if (parentSessionId !== undefined)
|
|
605
|
+
await this.setSessionParent(id, parentSessionId);
|
|
599
606
|
await this.sessionsStorage.update(id, {
|
|
600
|
-
...
|
|
607
|
+
...rest,
|
|
601
608
|
updatedAt: new Date()
|
|
602
609
|
});
|
|
603
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Place a session under a parent session (a subsession), or make it top
|
|
613
|
+
* level with `null`. The parent must exist, and may not be the session itself
|
|
614
|
+
* or any of its descendants: a cycle is refused `INVALID_INPUT
|
|
615
|
+
* session-parent-cycle`.
|
|
616
|
+
*/
|
|
617
|
+
async setSessionParent(sessionId, parentSessionId) {
|
|
618
|
+
await this.ensureStorage();
|
|
619
|
+
if (parentSessionId !== null) {
|
|
620
|
+
const seen = new Set();
|
|
621
|
+
for (let ancestor = parentSessionId; ancestor;) {
|
|
622
|
+
if (ancestor === sessionId || seen.has(ancestor)) {
|
|
623
|
+
throw new CapabilityError('A session cannot be placed under itself or one of its own subsessions.', 'INVALID_INPUT', {
|
|
624
|
+
reason: 'session-parent-cycle',
|
|
625
|
+
details: { sessionId, parentSessionId }
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
seen.add(ancestor);
|
|
629
|
+
const node = await this.sessionsStorage.get(ancestor);
|
|
630
|
+
if (!node && ancestor === parentSessionId) {
|
|
631
|
+
throw new CapabilityError('The parent session does not exist.', 'NOT_FOUND', {
|
|
632
|
+
reason: 'session-parent-not-found',
|
|
633
|
+
details: { parentSessionId }
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
// A dangling ancestor further up ends the chain; only the parent must exist.
|
|
637
|
+
ancestor = node?.parentSessionId;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
await this.sessionsStorage.update(sessionId, { parentSessionId, updatedAt: new Date() });
|
|
641
|
+
}
|
|
604
642
|
/**
|
|
605
643
|
* Bump a session's activity clock to reflect real traffic (messages), so
|
|
606
644
|
* `lastActivityAt` tracks when the session was last USED — not merely when its
|
|
@@ -1765,6 +1803,328 @@ export class SessionManager {
|
|
|
1765
1803
|
disconnectedAt: new Date()
|
|
1766
1804
|
});
|
|
1767
1805
|
}
|
|
1806
|
+
// INSTANCES
|
|
1807
|
+
//
|
|
1808
|
+
// One `session.connections` row per running instance, whatever surface it
|
|
1809
|
+
// came through. Live = not disconnected and `expiresAt` in the future. Every
|
|
1810
|
+
// admitted call touches its instance; a host that keeps a seat open while
|
|
1811
|
+
// idle registers it `hosted`, and this manager heartbeats hosted instances
|
|
1812
|
+
// until it closes them. A crashed host's instances simply expire.
|
|
1813
|
+
static INSTANCE_TTL_MS = 10 * 60 * 1000;
|
|
1814
|
+
static INSTANCE_HEARTBEAT_MS = 60 * 1000;
|
|
1815
|
+
/** Touches inside this window are not rewritten: one write per instance per window. */
|
|
1816
|
+
static INSTANCE_TOUCH_MIN_MS = 30 * 1000;
|
|
1817
|
+
static INSTANCE_HANDLE_ATTEMPTS = 6;
|
|
1818
|
+
/** This host's own key: distinguishes its in-process seats from another host's. */
|
|
1819
|
+
hostKey = crypto.randomUUID();
|
|
1820
|
+
hostedInstances = new Set();
|
|
1821
|
+
instanceTouchedAt = new Map();
|
|
1822
|
+
/** Handles of instances this host registered, so a touch need not reread them. */
|
|
1823
|
+
instanceHandles = new Map();
|
|
1824
|
+
/** (instance, session, participant) triples this host has already leased. */
|
|
1825
|
+
instanceMemberships = new Set();
|
|
1826
|
+
instanceHeartbeat;
|
|
1827
|
+
static isLiveInstance(row, nowIso) {
|
|
1828
|
+
const expiresAt = row.expiresAt instanceof Date ? row.expiresAt.toISOString() : row.expiresAt;
|
|
1829
|
+
return !row.disconnectedAt && expiresAt != null && String(expiresAt) >= nowIso;
|
|
1830
|
+
}
|
|
1831
|
+
static randomHandle(base) {
|
|
1832
|
+
const alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
1833
|
+
const bytes = crypto.getRandomValues(new Uint8Array(4));
|
|
1834
|
+
const suffix = Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');
|
|
1835
|
+
return `${sanitizeSlugCandidate(base).slice(0, 48)}-${suffix}`;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Register (or re-register) a running instance and give it a handle. A
|
|
1839
|
+
* chosen handle held by another LIVE instance is refused `CONFLICT
|
|
1840
|
+
* instance-handle-taken` naming the holder; one held by an expired or closed
|
|
1841
|
+
* instance is reclaimed. With no choice the instance keeps its live handle,
|
|
1842
|
+
* else gets a random `<agent>-<4>` handle. Uniqueness is the storage index
|
|
1843
|
+
* on `activeHandle`, not this check: a lost race is retried (random) or
|
|
1844
|
+
* refused (chosen).
|
|
1845
|
+
*/
|
|
1846
|
+
async registerInstance(input) {
|
|
1847
|
+
await this.ensureStorage();
|
|
1848
|
+
const storage = this.connectionsStorage;
|
|
1849
|
+
const now = Date.now();
|
|
1850
|
+
const nowAt = new Date(now);
|
|
1851
|
+
const nowIso = nowAt.toISOString();
|
|
1852
|
+
const expiresAt = new Date(now + SessionManager.INSTANCE_TTL_MS);
|
|
1853
|
+
const existing = await storage.get(input.instanceId);
|
|
1854
|
+
const chosen = input.handle?.trim() ? sanitizeSlugCandidate(input.handle) : undefined;
|
|
1855
|
+
// A live handle resolves ahead of participant slugs, so a chosen handle may
|
|
1856
|
+
// not be another participant's name: that would capture its messages.
|
|
1857
|
+
if (chosen) {
|
|
1858
|
+
const named = (await scanParticipantsBySlug(this, chosen)).match;
|
|
1859
|
+
const actsAs = input.participantId ?? existing?.participantId;
|
|
1860
|
+
if (named && named.participantId !== actsAs) {
|
|
1861
|
+
throw new CapabilityError(`The handle "${chosen}" is another participant's name.`, 'CONFLICT', {
|
|
1862
|
+
reason: 'instance-handle-names-participant',
|
|
1863
|
+
details: { handle: chosen, participantId: named.participantId },
|
|
1864
|
+
recoveryHint: 'Choose a handle that is not an existing participant\'s name.'
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const kept = existing && SessionManager.isLiveInstance(existing, nowIso) ? existing.activeHandle ?? undefined : undefined;
|
|
1869
|
+
const base = input.agentId ?? input.harness ?? input.clientName;
|
|
1870
|
+
const fields = {
|
|
1871
|
+
participantId: input.participantId ?? existing?.participantId,
|
|
1872
|
+
clientName: input.clientName,
|
|
1873
|
+
clientVersion: input.clientVersion,
|
|
1874
|
+
hostname: input.hostname,
|
|
1875
|
+
platform: input.platform,
|
|
1876
|
+
workingDirectory: input.workingDirectory,
|
|
1877
|
+
transport: input.transport,
|
|
1878
|
+
agentId: input.agentId,
|
|
1879
|
+
harness: input.harness,
|
|
1880
|
+
harnessSessionId: input.harnessSessionId,
|
|
1881
|
+
lastHeartbeatAt: nowAt,
|
|
1882
|
+
expiresAt,
|
|
1883
|
+
disconnectedAt: null
|
|
1884
|
+
};
|
|
1885
|
+
for (let attempt = 0; attempt < SessionManager.INSTANCE_HANDLE_ATTEMPTS; attempt++) {
|
|
1886
|
+
const handle = chosen ?? (attempt === 0 && kept ? kept : SessionManager.randomHandle(base));
|
|
1887
|
+
if (handle !== existing?.activeHandle)
|
|
1888
|
+
await this.releaseExpiredHandle(handle, input.instanceId, nowIso);
|
|
1889
|
+
const row = { ...fields, handle, activeHandle: handle };
|
|
1890
|
+
const written = await settle(() => existing
|
|
1891
|
+
? storage.update(input.instanceId, row)
|
|
1892
|
+
: storage.create({ connectionId: input.instanceId, connectedAt: nowAt, ...row }));
|
|
1893
|
+
if (written.ok) {
|
|
1894
|
+
if (input.hosted)
|
|
1895
|
+
this.hostInstance(input.instanceId);
|
|
1896
|
+
this.instanceTouchedAt.set(input.instanceId, now);
|
|
1897
|
+
return { ...existing, connectionId: input.instanceId, connectedAt: existing?.connectedAt ?? nowAt, ...row };
|
|
1898
|
+
}
|
|
1899
|
+
if (!isUniqueConstraintError(written.cause))
|
|
1900
|
+
throw written.cause;
|
|
1901
|
+
if (chosen) {
|
|
1902
|
+
const holder = await this.liveInstanceByHandle(chosen);
|
|
1903
|
+
throw new CapabilityError(`The handle "${chosen}" is held by another live instance.`, 'CONFLICT', {
|
|
1904
|
+
reason: 'instance-handle-taken',
|
|
1905
|
+
details: { handle: chosen, holder: holder?.connectionId ?? null, agentId: holder?.agentId ?? null },
|
|
1906
|
+
recoveryHint: 'Choose another handle, or omit it to be given a random one.'
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
throw new CapabilityError('Could not allocate a unique instance handle.', 'UNAVAILABLE', {
|
|
1911
|
+
reason: 'instance-handle-exhausted'
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
/** Free `handle` when its holder is no longer live, so a new instance can take it. */
|
|
1915
|
+
async releaseExpiredHandle(handle, claimant, nowIso) {
|
|
1916
|
+
const held = await this.connectionsStorage.list({
|
|
1917
|
+
where: { activeHandle: handle },
|
|
1918
|
+
limit: 1
|
|
1919
|
+
});
|
|
1920
|
+
const holder = held.items[0];
|
|
1921
|
+
if (!holder || holder.connectionId === claimant)
|
|
1922
|
+
return;
|
|
1923
|
+
if (SessionManager.isLiveInstance(holder, nowIso))
|
|
1924
|
+
return;
|
|
1925
|
+
await this.connectionsStorage.update(holder.connectionId, { activeHandle: null });
|
|
1926
|
+
}
|
|
1927
|
+
/** The live instance holding `handle`, if any. */
|
|
1928
|
+
async liveInstanceByHandle(handle) {
|
|
1929
|
+
await this.ensureStorage();
|
|
1930
|
+
const held = await this.connectionsStorage.list({
|
|
1931
|
+
where: { activeHandle: sanitizeSlugCandidate(handle) },
|
|
1932
|
+
limit: 1
|
|
1933
|
+
});
|
|
1934
|
+
const holder = held.items[0];
|
|
1935
|
+
return holder && SessionManager.isLiveInstance(holder, new Date().toISOString()) ? holder : null;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Mark an instance alive now and renew the membership leases it holds.
|
|
1939
|
+
* Writes at most once per touch window per instance.
|
|
1940
|
+
*/
|
|
1941
|
+
async touchInstance(instanceId, options = {}) {
|
|
1942
|
+
const now = Date.now();
|
|
1943
|
+
const last = this.instanceTouchedAt.get(instanceId);
|
|
1944
|
+
if (!options.force && last !== undefined && now - last < SessionManager.INSTANCE_TOUCH_MIN_MS)
|
|
1945
|
+
return true;
|
|
1946
|
+
await this.ensureStorage();
|
|
1947
|
+
const nowAt = new Date(now);
|
|
1948
|
+
const expiresAt = new Date(now + SessionManager.INSTANCE_TTL_MS);
|
|
1949
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
1950
|
+
if (!row || row.disconnectedAt) {
|
|
1951
|
+
this.forgetInstance(instanceId);
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
await this.connectionsStorage.update(instanceId, { lastHeartbeatAt: nowAt, expiresAt });
|
|
1955
|
+
this.instanceTouchedAt.set(instanceId, now);
|
|
1956
|
+
for (const lease of await this.unreleasedLeasesOf(instanceId)) {
|
|
1957
|
+
await this.membershipLeasesStorage.update(lease.id, {
|
|
1958
|
+
lastHeartbeatAt: nowAt.toISOString(),
|
|
1959
|
+
expiresAt: expiresAt.toISOString()
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
return true;
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Keep an admitted caller's instance live: register it the first time this
|
|
1966
|
+
* host sees it (or after it was closed), otherwise touch it.
|
|
1967
|
+
*/
|
|
1968
|
+
async ensureInstance(input) {
|
|
1969
|
+
const known = this.instanceHandles.get(input.instanceId);
|
|
1970
|
+
if (known !== undefined && (await this.touchInstance(input.instanceId)))
|
|
1971
|
+
return { handle: known };
|
|
1972
|
+
const row = await this.registerInstance(input);
|
|
1973
|
+
this.instanceHandles.set(input.instanceId, row.activeHandle ?? row.handle ?? '');
|
|
1974
|
+
return { handle: row.activeHandle ?? row.handle ?? undefined };
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Make an instance a member of the session it calls with: one lease keyed
|
|
1978
|
+
* on the instance, taken the first time this host sees the pair and renewed
|
|
1979
|
+
* by every touch after. An archived or missing session takes no member.
|
|
1980
|
+
*/
|
|
1981
|
+
async ensureInstanceMembership(input) {
|
|
1982
|
+
const key = `${input.instanceId}|${input.sessionId}|${input.participantId}`;
|
|
1983
|
+
if (this.instanceMemberships.has(key))
|
|
1984
|
+
return;
|
|
1985
|
+
const session = await this.getSession(input.sessionId);
|
|
1986
|
+
if (!session || session.status === 'archived')
|
|
1987
|
+
return;
|
|
1988
|
+
await this.acquireMembershipLease({
|
|
1989
|
+
sessionId: input.sessionId,
|
|
1990
|
+
participantId: input.participantId,
|
|
1991
|
+
connectionKey: input.instanceId
|
|
1992
|
+
});
|
|
1993
|
+
this.instanceMemberships.add(key);
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Give a live instance the handle it chose. The registry's uniqueness rule
|
|
1997
|
+
* applies: a handle another live instance holds is refused naming its holder.
|
|
1998
|
+
*/
|
|
1999
|
+
async chooseInstanceHandle(instanceId, handle) {
|
|
2000
|
+
await this.ensureStorage();
|
|
2001
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
2002
|
+
if (!row) {
|
|
2003
|
+
throw new CapabilityError('This caller has no registered instance to name.', 'NOT_FOUND', {
|
|
2004
|
+
reason: 'instance-unregistered',
|
|
2005
|
+
details: { instanceId }
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
const named = await this.registerInstance({
|
|
2009
|
+
instanceId,
|
|
2010
|
+
handle,
|
|
2011
|
+
transport: row.transport ?? 'unknown',
|
|
2012
|
+
clientName: row.clientName,
|
|
2013
|
+
clientVersion: row.clientVersion,
|
|
2014
|
+
participantId: row.participantId,
|
|
2015
|
+
agentId: row.agentId,
|
|
2016
|
+
harness: row.harness,
|
|
2017
|
+
harnessSessionId: row.harnessSessionId,
|
|
2018
|
+
hosted: this.hostedInstances.has(instanceId)
|
|
2019
|
+
});
|
|
2020
|
+
this.instanceHandles.set(instanceId, named.activeHandle ?? named.handle ?? '');
|
|
2021
|
+
return named;
|
|
2022
|
+
}
|
|
2023
|
+
/** One instance's registry row. */
|
|
2024
|
+
async getInstance(instanceId) {
|
|
2025
|
+
await this.ensureStorage();
|
|
2026
|
+
return this.connectionsStorage.get(instanceId);
|
|
2027
|
+
}
|
|
2028
|
+
/** Record the session an instance is working in (its bind, or its first write). */
|
|
2029
|
+
async setInstanceSession(instanceId, sessionId) {
|
|
2030
|
+
await this.ensureStorage();
|
|
2031
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
2032
|
+
if (!row || row.sessionId === sessionId)
|
|
2033
|
+
return;
|
|
2034
|
+
await this.connectionsStorage.update(instanceId, { sessionId });
|
|
2035
|
+
}
|
|
2036
|
+
/** The session an instance is working in, while that session is still open. */
|
|
2037
|
+
async instanceSession(instanceId) {
|
|
2038
|
+
await this.ensureStorage();
|
|
2039
|
+
const sessionId = (await this.connectionsStorage.get(instanceId))?.sessionId;
|
|
2040
|
+
if (!sessionId)
|
|
2041
|
+
return undefined;
|
|
2042
|
+
const session = await this.getSession(sessionId);
|
|
2043
|
+
return session && session.status !== 'archived' ? session.id : undefined;
|
|
2044
|
+
}
|
|
2045
|
+
/** Close an instance: it stops being live, frees its handle and releases its leases. */
|
|
2046
|
+
async closeInstance(instanceId) {
|
|
2047
|
+
await this.ensureStorage();
|
|
2048
|
+
this.hostedInstances.delete(instanceId);
|
|
2049
|
+
this.forgetInstance(instanceId);
|
|
2050
|
+
const nowAt = new Date();
|
|
2051
|
+
const row = await this.connectionsStorage.get(instanceId);
|
|
2052
|
+
if (row && !row.disconnectedAt) {
|
|
2053
|
+
await this.connectionsStorage.update(instanceId, { disconnectedAt: nowAt, activeHandle: null });
|
|
2054
|
+
}
|
|
2055
|
+
for (const lease of await this.unreleasedLeasesOf(instanceId)) {
|
|
2056
|
+
await this.membershipLeasesStorage.update(lease.id, {
|
|
2057
|
+
releasedAt: nowAt.toISOString(),
|
|
2058
|
+
activeConnectionKey: null
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
if (this.hostedInstances.size === 0)
|
|
2062
|
+
this.stopInstanceHeartbeat();
|
|
2063
|
+
}
|
|
2064
|
+
/** Every live instance in this store: who is here, however they arrived. */
|
|
2065
|
+
async listLiveInstances() {
|
|
2066
|
+
await this.ensureStorage();
|
|
2067
|
+
const nowAt = new Date();
|
|
2068
|
+
const nowIso = nowAt.toISOString();
|
|
2069
|
+
const live = [];
|
|
2070
|
+
const pageSize = 500;
|
|
2071
|
+
for (let offset = 0;; offset += pageSize) {
|
|
2072
|
+
const page = await this.connectionsStorage.list({
|
|
2073
|
+
where: { expiresAt: { gte: nowAt } },
|
|
2074
|
+
orderBy: { connectionId: 'asc' },
|
|
2075
|
+
limit: pageSize,
|
|
2076
|
+
offset
|
|
2077
|
+
});
|
|
2078
|
+
live.push(...page.items.filter((row) => SessionManager.isLiveInstance(row, nowIso)));
|
|
2079
|
+
if (page.items.length < pageSize)
|
|
2080
|
+
return live;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
forgetInstance(instanceId) {
|
|
2084
|
+
this.instanceTouchedAt.delete(instanceId);
|
|
2085
|
+
this.instanceHandles.delete(instanceId);
|
|
2086
|
+
for (const key of Array.from(this.instanceMemberships)) {
|
|
2087
|
+
if (key.startsWith(`${instanceId}|`))
|
|
2088
|
+
this.instanceMemberships.delete(key);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async unreleasedLeasesOf(instanceId) {
|
|
2092
|
+
const result = await this.membershipLeasesStorage.list({
|
|
2093
|
+
where: {
|
|
2094
|
+
activeConnectionKey: instanceId,
|
|
2095
|
+
releasedAt: { isNull: true }
|
|
2096
|
+
},
|
|
2097
|
+
limit: 500
|
|
2098
|
+
});
|
|
2099
|
+
return result.items;
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Heartbeat hosted instances on this runtime's lifecycle interval, so the
|
|
2103
|
+
* kernel stops it at disposal. A finite host (a one-shot command) runs no
|
|
2104
|
+
* interval: its instance lives by activity and closes with the manager.
|
|
2105
|
+
*/
|
|
2106
|
+
hostInstance(instanceId) {
|
|
2107
|
+
this.hostedInstances.add(instanceId);
|
|
2108
|
+
if (this.instanceHeartbeat)
|
|
2109
|
+
return;
|
|
2110
|
+
const runtime = this.hostRuntime;
|
|
2111
|
+
if (runtime.declaredScope.lifecycle === 'finite' || runtime.lifecycle.signal.aborted)
|
|
2112
|
+
return;
|
|
2113
|
+
this.instanceHeartbeat = runtime.lifecycle.interval(() => {
|
|
2114
|
+
for (const id of this.hostedInstances) {
|
|
2115
|
+
void settle(() => this.touchInstance(id, { force: true })).then((touched) => {
|
|
2116
|
+
if (touched.ok)
|
|
2117
|
+
return;
|
|
2118
|
+
this.logger.warn('Instance heartbeat failed', { instanceId: id, error: valueFreeFailure(touched.cause) });
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
}, SessionManager.INSTANCE_HEARTBEAT_MS);
|
|
2122
|
+
}
|
|
2123
|
+
stopInstanceHeartbeat() {
|
|
2124
|
+
const stop = this.instanceHeartbeat;
|
|
2125
|
+
this.instanceHeartbeat = undefined;
|
|
2126
|
+
stop?.();
|
|
2127
|
+
}
|
|
1768
2128
|
async getConnectionHistory(participantId, limit) {
|
|
1769
2129
|
await this.ensureStorage();
|
|
1770
2130
|
const where = {};
|