@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.
Files changed (89) hide show
  1. package/dist/admission/admit-caller.context.descriptor.d.ts +33 -0
  2. package/dist/admission/admit-caller.context.descriptor.d.ts.map +1 -0
  3. package/dist/admission/admit-caller.context.descriptor.js +10 -0
  4. package/dist/admission/admit-caller.context.descriptor.js.map +1 -0
  5. package/dist/admission/admit-caller.context.impl.d.ts +8 -0
  6. package/dist/admission/admit-caller.context.impl.d.ts.map +1 -0
  7. package/dist/admission/admit-caller.context.impl.js +15 -0
  8. package/dist/admission/admit-caller.context.impl.js.map +1 -0
  9. package/dist/admission/admit-caller.d.ts +57 -0
  10. package/dist/admission/admit-caller.d.ts.map +1 -0
  11. package/dist/admission/admit-caller.js +163 -0
  12. package/dist/admission/admit-caller.js.map +1 -0
  13. package/dist/cli/services/session/sessions.d.ts +4 -4
  14. package/dist/cli/services/session.cli.consumer.d.ts +1 -1
  15. package/dist/manager.d.ts +87 -2
  16. package/dist/manager.d.ts.map +1 -1
  17. package/dist/manager.js +363 -3
  18. package/dist/manager.js.map +1 -1
  19. package/dist/mcp/schemas/tools.d.ts +3 -0
  20. package/dist/mcp/schemas/tools.d.ts.map +1 -1
  21. package/dist/mcp/schemas/tools.js +15 -2
  22. package/dist/mcp/schemas/tools.js.map +1 -1
  23. package/dist/mcp/utils/participant.d.ts +25 -8
  24. package/dist/mcp/utils/participant.d.ts.map +1 -1
  25. package/dist/mcp/utils/participant.js +26 -35
  26. package/dist/mcp/utils/participant.js.map +1 -1
  27. package/dist/schemas/index.d.ts +2 -2
  28. package/dist/schemas/index.d.ts.map +1 -1
  29. package/dist/schemas/index.js +1 -1
  30. package/dist/schemas/index.js.map +1 -1
  31. package/dist/schemas/participant.d.ts +30 -0
  32. package/dist/schemas/participant.d.ts.map +1 -1
  33. package/dist/schemas/participant.js +38 -1
  34. package/dist/schemas/participant.js.map +1 -1
  35. package/dist/schemas/session.d.ts +5 -5
  36. package/dist/schemas/session.js +1 -1
  37. package/dist/schemas/session.js.map +1 -1
  38. package/dist/session.plugin.d.ts.map +1 -1
  39. package/dist/session.plugin.js +3 -0
  40. package/dist/session.plugin.js.map +1 -1
  41. package/dist/storage/session-connections.record.descriptor.d.ts +9 -0
  42. package/dist/storage/session-connections.record.descriptor.d.ts.map +1 -1
  43. package/dist/storage/session-connections.record.descriptor.js +23 -2
  44. package/dist/storage/session-connections.record.descriptor.js.map +1 -1
  45. package/dist/storage/session-sessions.record.descriptor.d.ts +1 -1
  46. package/dist/tools/lifecycle/context/handler.d.ts +1 -1
  47. package/dist/tools/lifecycle/init/handler.d.ts.map +1 -1
  48. package/dist/tools/lifecycle/init/handler.js +16 -0
  49. package/dist/tools/lifecycle/init/handler.js.map +1 -1
  50. package/dist/tools/lifecycle/init/schemas.d.ts +7 -0
  51. package/dist/tools/lifecycle/init/schemas.d.ts.map +1 -1
  52. package/dist/tools/lifecycle/init/schemas.js +14 -1
  53. package/dist/tools/lifecycle/init/schemas.js.map +1 -1
  54. package/dist/tools/lifecycle/init/types.d.ts +6 -0
  55. package/dist/tools/lifecycle/init/types.d.ts.map +1 -1
  56. package/dist/tools/lifecycle/init/types.js.map +1 -1
  57. package/dist/tools/lifecycle/packet/handler.d.ts +2 -2
  58. package/dist/tools/lifecycle/snapshot/handler.d.ts +1 -1
  59. package/dist/tools/lifecycle/state/status/handler.d.ts +1 -1
  60. package/dist/tools/lifecycle/tool/schemas/index.d.ts +3 -0
  61. package/dist/tools/lifecycle/tool/schemas/index.d.ts.map +1 -1
  62. package/dist/tools/lifecycle/tool/session.tool.descriptor.js +5 -5
  63. package/dist/tools/lifecycle/tool/session.tool.descriptor.js.map +1 -1
  64. package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.d.ts +2 -0
  65. package/dist/tools/lifecycle/tool/session.variant.init.impl.cloud.d.ts.map +1 -1
  66. package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.d.ts +2 -0
  67. package/dist/tools/lifecycle/tool/session.variant.init.impl.consumer.d.ts.map +1 -1
  68. package/dist/tools/lifecycle/tool/session.variant.update.impl.d.ts +1 -0
  69. package/dist/tools/lifecycle/tool/session.variant.update.impl.d.ts.map +1 -1
  70. package/dist/tools/lifecycle/update/handler.d.ts +2 -2
  71. package/dist/tools/lifecycle/update/handler.d.ts.map +1 -1
  72. package/dist/tools/lifecycle/update/handler.js +13 -0
  73. package/dist/tools/lifecycle/update/handler.js.map +1 -1
  74. package/dist/tools/lifecycle/update/schemas.d.ts +1 -0
  75. package/dist/tools/lifecycle/update/schemas.d.ts.map +1 -1
  76. package/dist/tools/lifecycle/update/schemas.js +3 -1
  77. package/dist/tools/lifecycle/update/schemas.js.map +1 -1
  78. package/dist/tools/messages/handlers.d.ts.map +1 -1
  79. package/dist/tools/messages/handlers.js +4 -2
  80. package/dist/tools/messages/handlers.js.map +1 -1
  81. package/dist/tools/messages/tool/session-collaborate.descriptor.js +1 -1
  82. package/dist/tools/messages/tool/session-collaborate.descriptor.js.map +1 -1
  83. package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.js +4 -4
  84. package/dist/tools/scratchpad/tool/session-scratchpad.descriptor.js.map +1 -1
  85. package/dist/tools/todos/tool/session-todo.descriptor.js +3 -3
  86. package/dist/tools/todos/tool/session-todo.descriptor.js.map +1 -1
  87. package/dist/tools/workspace/tool/session-workspace.descriptor.js +1 -1
  88. package/dist/tools/workspace/tool/session-workspace.descriptor.js.map +1 -1
  89. 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
- ...updates,
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 = {};