@memberjunction/server 5.40.2 → 5.42.0
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/agentSessions/HostInstance.d.ts +19 -0
- package/dist/agentSessions/HostInstance.d.ts.map +1 -0
- package/dist/agentSessions/HostInstance.js +48 -0
- package/dist/agentSessions/HostInstance.js.map +1 -0
- package/dist/agentSessions/SessionJanitor.d.ts +97 -0
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -0
- package/dist/agentSessions/SessionJanitor.js +222 -0
- package/dist/agentSessions/SessionJanitor.js.map +1 -0
- package/dist/agentSessions/SessionManager.d.ts +142 -0
- package/dist/agentSessions/SessionManager.d.ts.map +1 -0
- package/dist/agentSessions/SessionManager.js +308 -0
- package/dist/agentSessions/SessionManager.js.map +1 -0
- package/dist/agentSessions/index.d.ts +5 -0
- package/dist/agentSessions/index.d.ts.map +1 -0
- package/dist/agentSessions/index.js +31 -0
- package/dist/agentSessions/index.js.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts +102 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js +164 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts +66 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js +86 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js.map +1 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +4 -2
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -2
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +41 -7
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +1296 -49
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +50343 -43421
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.js +1 -1
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.js +9 -10
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +127 -54
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +108 -0
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -0
- package/dist/integration/CustomColumnPromoter.js +508 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -0
- package/dist/logging/StartupLogger.d.ts +177 -0
- package/dist/logging/StartupLogger.d.ts.map +1 -0
- package/dist/logging/StartupLogger.js +354 -0
- package/dist/logging/StartupLogger.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.js +21 -2
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
- package/dist/resolvers/AgentSessionResolver.d.ts +42 -0
- package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -0
- package/dist/resolvers/AgentSessionResolver.js +152 -0
- package/dist/resolvers/AgentSessionResolver.js.map +1 -0
- package/dist/resolvers/EntityPermissionResolver.d.ts +16 -0
- package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -0
- package/dist/resolvers/EntityPermissionResolver.js +95 -0
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +27 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +136 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +101 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +544 -42
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +121 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.js +533 -0
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +688 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js +1774 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +432 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.js +1108 -0
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +0 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +5 -3
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +6 -5
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/package.json +83 -74
- package/src/__tests__/RealtimeBridgeResolver.test.ts +100 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +2605 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +181 -0
- package/src/__tests__/RemoteBrowserGoalEngine.test.ts +144 -0
- package/src/__tests__/SessionJanitor.test.ts +234 -0
- package/src/__tests__/SessionManager.test.ts +465 -0
- package/src/__tests__/remoteBrowserGoalRegistry.test.ts +81 -0
- package/src/__tests__/subscriptionRedaction.test.ts +5 -0
- package/src/agentSessions/HostInstance.ts +53 -0
- package/src/agentSessions/SessionJanitor.ts +267 -0
- package/src/agentSessions/SessionManager.ts +446 -0
- package/src/agentSessions/index.ts +32 -0
- package/src/agentSessions/remoteBrowserGoalEngine.ts +191 -0
- package/src/agentSessions/remoteBrowserGoalRegistry.ts +122 -0
- package/src/auth/initializeProviders.ts +4 -2
- package/src/config.ts +13 -2
- package/src/context.ts +42 -7
- package/src/generated/generated.ts +33808 -29025
- package/src/generic/ResolverBase.ts +1 -1
- package/src/generic/RunViewResolver.ts +9 -9
- package/src/index.ts +128 -54
- package/src/integration/CustomColumnPromoter.ts +597 -0
- package/src/logging/StartupLogger.ts +441 -0
- package/src/logging/variablesLoggingMiddleware.ts +25 -5
- package/src/resolvers/AgentSessionResolver.ts +138 -0
- package/src/resolvers/EntityPermissionResolver.ts +73 -0
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +108 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +396 -37
- package/src/resolvers/RealtimeBridgeResolver.ts +450 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +2162 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +1122 -0
- package/src/resolvers/RunAIAgentResolver.ts +1 -9
- package/src/rest/setupRESTEndpoints.ts +5 -3
- package/src/services/ScheduledJobsService.ts +6 -5
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns this process's stable host-instance identity (`hostname:pid:bootId`).
|
|
3
|
+
* Stamped into `AIAgentSession.HostInstanceID` at session create and read back by the
|
|
4
|
+
* janitor for own-host orphan recovery.
|
|
5
|
+
*/
|
|
6
|
+
export declare function GetHostInstanceID(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the host-name prefix (`hostname:`) used to match *any* session previously hosted
|
|
9
|
+
* by this OS host, regardless of which boot created it. The janitor uses this to find rows
|
|
10
|
+
* that belong to this host but to a *different* (older) boot — i.e. crash/redeploy orphans.
|
|
11
|
+
*/
|
|
12
|
+
export declare function GetHostNamePrefix(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the UUID generated once at process start. Exposed primarily for diagnostics and
|
|
15
|
+
* tests; the janitor compares against the full {@link GetHostInstanceID} rather than this
|
|
16
|
+
* value directly.
|
|
17
|
+
*/
|
|
18
|
+
export declare function GetBootID(): string;
|
|
19
|
+
//# sourceMappingURL=HostInstance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostInstance.d.ts","sourceRoot":"","sources":["../../src/agentSessions/HostInstance.ts"],"names":[],"mappings":"AA2BA;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { hostname } from 'os';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Stable, per-process identity for this server node, used to anchor
|
|
5
|
+
* {@link import('@memberjunction/core-entities').MJAIAgentSessionEntity} rows to the
|
|
6
|
+
* instance that currently owns their in-memory sockets.
|
|
7
|
+
*
|
|
8
|
+
* The shape is `${hostname}:${pid}:${bootId}` where:
|
|
9
|
+
* - `hostname` and `pid` locate the OS process,
|
|
10
|
+
* - `bootId` is a UUID generated **once** at module load, so two different boots of the
|
|
11
|
+
* *same* host (a crash/redeploy that reuses pid, or a long-lived host that restarts the
|
|
12
|
+
* process) produce **distinct** identities.
|
|
13
|
+
*
|
|
14
|
+
* This last property is what lets {@link import('./SessionJanitor.js').SessionJanitor} tell a
|
|
15
|
+
* live session owned by *this* boot apart from an orphan left by a *previous* boot of the
|
|
16
|
+
* same host: only the latter has a `HostInstanceID` that starts with `${hostname}:` but
|
|
17
|
+
* carries a different `bootId`.
|
|
18
|
+
*/
|
|
19
|
+
const BOOT_ID = randomUUID();
|
|
20
|
+
/** Resolved host name captured once at load (cheap; avoids repeated syscalls). */
|
|
21
|
+
const HOST_NAME = hostname();
|
|
22
|
+
/** The full, immutable host-instance identity string for this process. */
|
|
23
|
+
const HOST_INSTANCE_ID = `${HOST_NAME}:${process.pid}:${BOOT_ID}`;
|
|
24
|
+
/**
|
|
25
|
+
* Returns this process's stable host-instance identity (`hostname:pid:bootId`).
|
|
26
|
+
* Stamped into `AIAgentSession.HostInstanceID` at session create and read back by the
|
|
27
|
+
* janitor for own-host orphan recovery.
|
|
28
|
+
*/
|
|
29
|
+
export function GetHostInstanceID() {
|
|
30
|
+
return HOST_INSTANCE_ID;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the host-name prefix (`hostname:`) used to match *any* session previously hosted
|
|
34
|
+
* by this OS host, regardless of which boot created it. The janitor uses this to find rows
|
|
35
|
+
* that belong to this host but to a *different* (older) boot — i.e. crash/redeploy orphans.
|
|
36
|
+
*/
|
|
37
|
+
export function GetHostNamePrefix() {
|
|
38
|
+
return `${HOST_NAME}:`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the UUID generated once at process start. Exposed primarily for diagnostics and
|
|
42
|
+
* tests; the janitor compares against the full {@link GetHostInstanceID} rather than this
|
|
43
|
+
* value directly.
|
|
44
|
+
*/
|
|
45
|
+
export function GetBootID() {
|
|
46
|
+
return BOOT_ID;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=HostInstance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostInstance.js","sourceRoot":"","sources":["../../src/agentSessions/HostInstance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;AAE7B,kFAAkF;AAClF,MAAM,SAAS,GAAG,QAAQ,EAAE,CAAC;AAE7B,0EAA0E;AAC1E,MAAM,gBAAgB,GAAG,GAAG,SAAS,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,EAAE,CAAC;AAElE;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC7B,OAAO,gBAAgB,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC7B,OAAO,GAAG,SAAS,GAAG,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS;IACrB,OAAO,OAAO,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { BaseSingleton, IShutdownable } from '@memberjunction/global';
|
|
2
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
3
|
+
/** Tuning knobs for the janitor. All durations in their named units. */
|
|
4
|
+
export interface SessionJanitorConfig {
|
|
5
|
+
/** A non-`Closed` session whose `LastActiveAt` is older than this is force-closed by the global sweep. */
|
|
6
|
+
closeThresholdMinutes: number;
|
|
7
|
+
/** How often the periodic staleness sweep runs once {@link SessionJanitor.Start} is called. */
|
|
8
|
+
sweepIntervalMs: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `BaseSingleton` background reconciler that keeps the durable `AIAgentSession` state from drifting
|
|
12
|
+
* away from volatile process reality. A crash/redeploy vaporizes a host's in-memory sockets but
|
|
13
|
+
* leaves its session rows reading `Active`/`Idle` forever; the janitor force-closes those orphans.
|
|
14
|
+
*
|
|
15
|
+
* Two sweeps, both writing through {@link SessionManager.CloseSession} so Record Changes captures
|
|
16
|
+
* each transition and channel rows are disconnected consistently:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Own-host recovery ({@link RunStartupRecovery})** — run once at boot. Closes any `Active`/`Idle`
|
|
19
|
+
* session whose `HostInstanceID` belongs to a *previous* boot of *this* host (same hostname prefix,
|
|
20
|
+
* different `bootId`). Primary defense against the "Active forever" leak after a restart.
|
|
21
|
+
* 2. **Global staleness sweep ({@link RunStalenessSweep})** — run periodically on every instance.
|
|
22
|
+
* Closes any `Active`/`Idle` session whose `LastActiveAt` is older than `closeThresholdMinutes`,
|
|
23
|
+
* regardless of host. Catches sessions whose owner died without a clean reboot (OOM, scaled-down pod).
|
|
24
|
+
*
|
|
25
|
+
* Both sweeps stamp `CloseReason = 'Janitor'`. A third, shutdown-time path —
|
|
26
|
+
* {@link RunShutdownDrain}, invoked from {@link Shutdown} during the graceful ShutdownRegistry
|
|
27
|
+
* drain — closes this exact host instance's own live sessions with `CloseReason = 'Shutdown'`.
|
|
28
|
+
*
|
|
29
|
+
* Both sweeps are **idempotent and safe to run concurrently** on every instance: closing an
|
|
30
|
+
* already-`Closed` session is a no-op, and the close path is last-writer-wins. Both page with keyset
|
|
31
|
+
* (`AfterKey`) pagination per the deep-pagination guide so a large backlog can't blow up memory.
|
|
32
|
+
*/
|
|
33
|
+
export declare class SessionJanitor extends BaseSingleton<SessionJanitor> implements IShutdownable {
|
|
34
|
+
private _config;
|
|
35
|
+
private _sweepTimer;
|
|
36
|
+
private _sweepRunning;
|
|
37
|
+
private _registered;
|
|
38
|
+
private _provider;
|
|
39
|
+
private _systemUser;
|
|
40
|
+
private readonly sessionManager;
|
|
41
|
+
protected constructor();
|
|
42
|
+
/** Process-wide singleton accessor (Global Object Store backed via {@link BaseSingleton}). */
|
|
43
|
+
static get Instance(): SessionJanitor;
|
|
44
|
+
/** Identifier surfaced in graceful-shutdown logs. */
|
|
45
|
+
readonly ShutdownName = "SessionJanitor";
|
|
46
|
+
/** Overrides default tuning. Safe to call before or after {@link Start}. */
|
|
47
|
+
Configure(config: Partial<SessionJanitorConfig>): void;
|
|
48
|
+
/**
|
|
49
|
+
* Run own-host startup recovery once, then schedule the periodic staleness sweep. Idempotent:
|
|
50
|
+
* a second call does not stack a second timer. Captures the provider + system user so the timer
|
|
51
|
+
* callback can run without re-supplying them.
|
|
52
|
+
*/
|
|
53
|
+
Start(provider: IMetadataProvider, systemUser: UserInfo, intervalMs?: number): Promise<void>;
|
|
54
|
+
/** Stops the periodic sweep timer. Idempotent. Part of {@link IShutdownable}. */
|
|
55
|
+
Stop(): void;
|
|
56
|
+
/**
|
|
57
|
+
* {@link IShutdownable}: clear the timer, then drain this host's own live sessions so a
|
|
58
|
+
* graceful stop never strands `Active`/`Idle` rows for the next boot's janitor to mop up.
|
|
59
|
+
* Drained sessions are stamped `CloseReason = 'Shutdown'` (vs. `'Janitor'` for crash orphans),
|
|
60
|
+
* so the dashboards can tell a clean redeploy from a reconciled crash. Never throws; the drain
|
|
61
|
+
* is skipped when {@link Start} was never called (no captured provider/user).
|
|
62
|
+
*/
|
|
63
|
+
Shutdown(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Close every `Active`/`Idle` session owned by **this exact host instance** (current boot),
|
|
66
|
+
* stamping `CloseReason = 'Shutdown'`. Invoked from {@link Shutdown} during the graceful
|
|
67
|
+
* shutdown drain; exposed publicly for tests and for hosts that want to drain explicitly.
|
|
68
|
+
* Returns the number of sessions closed.
|
|
69
|
+
*/
|
|
70
|
+
RunShutdownDrain(provider: IMetadataProvider, systemUser: UserInfo): Promise<number>;
|
|
71
|
+
/**
|
|
72
|
+
* Force-close any `Active`/`Idle` session left behind by a *previous boot of this host*
|
|
73
|
+
* (matching hostname prefix, differing `bootId`). Returns the number of sessions closed.
|
|
74
|
+
*/
|
|
75
|
+
RunStartupRecovery(provider: IMetadataProvider, systemUser: UserInfo): Promise<number>;
|
|
76
|
+
/**
|
|
77
|
+
* Force-close any `Active`/`Idle` session whose `LastActiveAt` is older than the configured
|
|
78
|
+
* close threshold, regardless of host. Idempotent and concurrency-safe. Returns the count closed.
|
|
79
|
+
*/
|
|
80
|
+
RunStalenessSweep(provider: IMetadataProvider, systemUser: UserInfo): Promise<number>;
|
|
81
|
+
/** Register for graceful shutdown exactly once. */
|
|
82
|
+
private ensureRegistered;
|
|
83
|
+
/** Schedule the periodic staleness sweep, exactly once. Timer is unref'd so it never blocks exit. */
|
|
84
|
+
private scheduleSweep;
|
|
85
|
+
/** Timer tick: run the staleness sweep under the captured provider/user, guarding overlap. */
|
|
86
|
+
private periodicSweep;
|
|
87
|
+
/**
|
|
88
|
+
* Page through every session matching `filter` with keyset (`AfterKey`) pagination and close each
|
|
89
|
+
* via {@link SessionManager.CloseSession}, stamping `closeReason` on every row it transitions.
|
|
90
|
+
* Returns the number successfully closed. A `Closed`-by-now row (raced by another instance) is a
|
|
91
|
+
* harmless no-op that keeps its original reason.
|
|
92
|
+
*/
|
|
93
|
+
private sweepAndClose;
|
|
94
|
+
/** Fetch one keyset page of matching sessions, or null on a load failure (logged). */
|
|
95
|
+
private fetchPage;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=SessionJanitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionJanitor.d.ts","sourceRoot":"","sources":["../../src/agentSessions/SessionJanitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAoB,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACH,iBAAiB,EACjB,QAAQ,EAMX,MAAM,sBAAsB,CAAC;AAW9B,wEAAwE;AACxE,MAAM,WAAW,oBAAoB;IACjC,0GAA0G;IAC1G,qBAAqB,EAAE,MAAM,CAAC;IAC9B,+FAA+F;IAC/F,eAAe,EAAE,MAAM,CAAC;CAC3B;AAOD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,cAAe,SAAQ,aAAa,CAAC,cAAc,CAAE,YAAW,aAAa;IACtF,OAAO,CAAC,OAAO,CAAwC;IACvD,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IAEvD,SAAS;IAIT,8FAA8F;IAC9F,WAAkB,QAAQ,IAAI,cAAc,CAE3C;IAED,qDAAqD;IACrD,SAAgB,YAAY,oBAAoB;IAEhD,4EAA4E;IACrE,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI;IAI7D;;;;OAIG;IACU,KAAK,CAAC,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWzG,iFAAiF;IAC1E,IAAI,IAAI,IAAI;IAOnB;;;;;;OAMG;IACU,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtC;;;;;OAKG;IACU,gBAAgB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAUjG;;;OAGG;IACU,kBAAkB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAcnG;;;OAGG;IACU,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAYlG,mDAAmD;IACnD,OAAO,CAAC,gBAAgB;IAOxB,qGAAqG;IACrG,OAAO,CAAC,aAAa;IAQrB,8FAA8F;YAChF,aAAa;IAc3B;;;;;OAKG;YACW,aAAa;IAgC3B,sFAAsF;YACxE,SAAS;CAqB1B"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { BaseSingleton, ShutdownRegistry } from '@memberjunction/global';
|
|
2
|
+
import { CompositeKey, RunView, LogError, LogStatus, } from '@memberjunction/core';
|
|
3
|
+
import { GetHostInstanceID, GetHostNamePrefix } from './HostInstance.js';
|
|
4
|
+
import { SessionManager } from './SessionManager.js';
|
|
5
|
+
/** Entity name for session rows (kept in sync with {@link SessionManager}). */
|
|
6
|
+
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
7
|
+
/** Rows fetched per keyset page during a sweep. Small enough to bound memory on a large backlog. */
|
|
8
|
+
const SWEEP_PAGE_SIZE = 200;
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
closeThresholdMinutes: 15,
|
|
11
|
+
sweepIntervalMs: 60_000,
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* `BaseSingleton` background reconciler that keeps the durable `AIAgentSession` state from drifting
|
|
15
|
+
* away from volatile process reality. A crash/redeploy vaporizes a host's in-memory sockets but
|
|
16
|
+
* leaves its session rows reading `Active`/`Idle` forever; the janitor force-closes those orphans.
|
|
17
|
+
*
|
|
18
|
+
* Two sweeps, both writing through {@link SessionManager.CloseSession} so Record Changes captures
|
|
19
|
+
* each transition and channel rows are disconnected consistently:
|
|
20
|
+
*
|
|
21
|
+
* 1. **Own-host recovery ({@link RunStartupRecovery})** — run once at boot. Closes any `Active`/`Idle`
|
|
22
|
+
* session whose `HostInstanceID` belongs to a *previous* boot of *this* host (same hostname prefix,
|
|
23
|
+
* different `bootId`). Primary defense against the "Active forever" leak after a restart.
|
|
24
|
+
* 2. **Global staleness sweep ({@link RunStalenessSweep})** — run periodically on every instance.
|
|
25
|
+
* Closes any `Active`/`Idle` session whose `LastActiveAt` is older than `closeThresholdMinutes`,
|
|
26
|
+
* regardless of host. Catches sessions whose owner died without a clean reboot (OOM, scaled-down pod).
|
|
27
|
+
*
|
|
28
|
+
* Both sweeps stamp `CloseReason = 'Janitor'`. A third, shutdown-time path —
|
|
29
|
+
* {@link RunShutdownDrain}, invoked from {@link Shutdown} during the graceful ShutdownRegistry
|
|
30
|
+
* drain — closes this exact host instance's own live sessions with `CloseReason = 'Shutdown'`.
|
|
31
|
+
*
|
|
32
|
+
* Both sweeps are **idempotent and safe to run concurrently** on every instance: closing an
|
|
33
|
+
* already-`Closed` session is a no-op, and the close path is last-writer-wins. Both page with keyset
|
|
34
|
+
* (`AfterKey`) pagination per the deep-pagination guide so a large backlog can't blow up memory.
|
|
35
|
+
*/
|
|
36
|
+
export class SessionJanitor extends BaseSingleton {
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
this._config = DEFAULT_CONFIG;
|
|
40
|
+
this._sweepTimer = null;
|
|
41
|
+
this._sweepRunning = false;
|
|
42
|
+
this._registered = false;
|
|
43
|
+
this._provider = null;
|
|
44
|
+
this._systemUser = null;
|
|
45
|
+
this.sessionManager = new SessionManager();
|
|
46
|
+
/** Identifier surfaced in graceful-shutdown logs. */
|
|
47
|
+
this.ShutdownName = 'SessionJanitor';
|
|
48
|
+
}
|
|
49
|
+
/** Process-wide singleton accessor (Global Object Store backed via {@link BaseSingleton}). */
|
|
50
|
+
static get Instance() {
|
|
51
|
+
return super.getInstance();
|
|
52
|
+
}
|
|
53
|
+
/** Overrides default tuning. Safe to call before or after {@link Start}. */
|
|
54
|
+
Configure(config) {
|
|
55
|
+
this._config = { ...this._config, ...config };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run own-host startup recovery once, then schedule the periodic staleness sweep. Idempotent:
|
|
59
|
+
* a second call does not stack a second timer. Captures the provider + system user so the timer
|
|
60
|
+
* callback can run without re-supplying them.
|
|
61
|
+
*/
|
|
62
|
+
async Start(provider, systemUser, intervalMs) {
|
|
63
|
+
this._provider = provider;
|
|
64
|
+
this._systemUser = systemUser;
|
|
65
|
+
if (intervalMs != null) {
|
|
66
|
+
this._config = { ...this._config, sweepIntervalMs: intervalMs };
|
|
67
|
+
}
|
|
68
|
+
this.ensureRegistered();
|
|
69
|
+
await this.RunStartupRecovery(provider, systemUser);
|
|
70
|
+
this.scheduleSweep();
|
|
71
|
+
}
|
|
72
|
+
/** Stops the periodic sweep timer. Idempotent. Part of {@link IShutdownable}. */
|
|
73
|
+
Stop() {
|
|
74
|
+
if (this._sweepTimer) {
|
|
75
|
+
clearInterval(this._sweepTimer);
|
|
76
|
+
this._sweepTimer = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* {@link IShutdownable}: clear the timer, then drain this host's own live sessions so a
|
|
81
|
+
* graceful stop never strands `Active`/`Idle` rows for the next boot's janitor to mop up.
|
|
82
|
+
* Drained sessions are stamped `CloseReason = 'Shutdown'` (vs. `'Janitor'` for crash orphans),
|
|
83
|
+
* so the dashboards can tell a clean redeploy from a reconciled crash. Never throws; the drain
|
|
84
|
+
* is skipped when {@link Start} was never called (no captured provider/user).
|
|
85
|
+
*/
|
|
86
|
+
async Shutdown() {
|
|
87
|
+
this.Stop();
|
|
88
|
+
if (this._provider && this._systemUser) {
|
|
89
|
+
try {
|
|
90
|
+
await this.RunShutdownDrain(this._provider, this._systemUser);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
LogError(`SessionJanitor shutdown drain failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Close every `Active`/`Idle` session owned by **this exact host instance** (current boot),
|
|
99
|
+
* stamping `CloseReason = 'Shutdown'`. Invoked from {@link Shutdown} during the graceful
|
|
100
|
+
* shutdown drain; exposed publicly for tests and for hosts that want to drain explicitly.
|
|
101
|
+
* Returns the number of sessions closed.
|
|
102
|
+
*/
|
|
103
|
+
async RunShutdownDrain(provider, systemUser) {
|
|
104
|
+
const current = GetHostInstanceID().replace(/'/g, "''");
|
|
105
|
+
const filter = `Status IN ('Active','Idle') AND HostInstanceID = '${current}'`;
|
|
106
|
+
const closed = await this.sweepAndClose(filter, provider, systemUser, 'Shutdown');
|
|
107
|
+
if (closed > 0) {
|
|
108
|
+
LogStatus(`[SessionJanitor] Shutdown drain closed ${closed} live session(s) owned by this host instance`);
|
|
109
|
+
}
|
|
110
|
+
return closed;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Force-close any `Active`/`Idle` session left behind by a *previous boot of this host*
|
|
114
|
+
* (matching hostname prefix, differing `bootId`). Returns the number of sessions closed.
|
|
115
|
+
*/
|
|
116
|
+
async RunStartupRecovery(provider, systemUser) {
|
|
117
|
+
const prefix = GetHostNamePrefix().replace(/'/g, "''");
|
|
118
|
+
const current = GetHostInstanceID().replace(/'/g, "''");
|
|
119
|
+
const filter = `Status IN ('Active','Idle') ` +
|
|
120
|
+
`AND HostInstanceID LIKE '${prefix}%' ` +
|
|
121
|
+
`AND HostInstanceID <> '${current}'`;
|
|
122
|
+
const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor');
|
|
123
|
+
if (closed > 0) {
|
|
124
|
+
LogStatus(`[SessionJanitor] Startup recovery closed ${closed} orphaned session(s) from a prior boot of this host`);
|
|
125
|
+
}
|
|
126
|
+
return closed;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Force-close any `Active`/`Idle` session whose `LastActiveAt` is older than the configured
|
|
130
|
+
* close threshold, regardless of host. Idempotent and concurrency-safe. Returns the count closed.
|
|
131
|
+
*/
|
|
132
|
+
async RunStalenessSweep(provider, systemUser) {
|
|
133
|
+
const cutoffIso = new Date(Date.now() - this._config.closeThresholdMinutes * 60_000).toISOString();
|
|
134
|
+
const filter = `Status IN ('Active','Idle') AND LastActiveAt < '${cutoffIso}'`;
|
|
135
|
+
const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor');
|
|
136
|
+
if (closed > 0) {
|
|
137
|
+
LogStatus(`[SessionJanitor] Staleness sweep closed ${closed} stale session(s) (>${this._config.closeThresholdMinutes}m idle)`);
|
|
138
|
+
}
|
|
139
|
+
return closed;
|
|
140
|
+
}
|
|
141
|
+
// ----- internals -------------------------------------------------------------------------
|
|
142
|
+
/** Register for graceful shutdown exactly once. */
|
|
143
|
+
ensureRegistered() {
|
|
144
|
+
if (!this._registered) {
|
|
145
|
+
ShutdownRegistry.Instance.Register(this);
|
|
146
|
+
this._registered = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Schedule the periodic staleness sweep, exactly once. Timer is unref'd so it never blocks exit. */
|
|
150
|
+
scheduleSweep() {
|
|
151
|
+
if (this._sweepTimer) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this._sweepTimer = setInterval(() => void this.periodicSweep(), this._config.sweepIntervalMs);
|
|
155
|
+
this._sweepTimer.unref?.();
|
|
156
|
+
}
|
|
157
|
+
/** Timer tick: run the staleness sweep under the captured provider/user, guarding overlap. */
|
|
158
|
+
async periodicSweep() {
|
|
159
|
+
if (this._sweepRunning || !this._provider || !this._systemUser) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this._sweepRunning = true;
|
|
163
|
+
try {
|
|
164
|
+
await this.RunStalenessSweep(this._provider, this._systemUser);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
LogError(`SessionJanitor periodic sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
this._sweepRunning = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Page through every session matching `filter` with keyset (`AfterKey`) pagination and close each
|
|
175
|
+
* via {@link SessionManager.CloseSession}, stamping `closeReason` on every row it transitions.
|
|
176
|
+
* Returns the number successfully closed. A `Closed`-by-now row (raced by another instance) is a
|
|
177
|
+
* harmless no-op that keeps its original reason.
|
|
178
|
+
*/
|
|
179
|
+
async sweepAndClose(filter, provider, systemUser, closeReason) {
|
|
180
|
+
let closedCount = 0;
|
|
181
|
+
let afterKey;
|
|
182
|
+
// eslint-disable-next-line no-constant-condition
|
|
183
|
+
while (true) {
|
|
184
|
+
const page = await this.fetchPage(filter, afterKey, provider, systemUser);
|
|
185
|
+
if (page == null) {
|
|
186
|
+
break; // load failure already logged
|
|
187
|
+
}
|
|
188
|
+
if (page.length === 0) {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
for (const session of page) {
|
|
192
|
+
const closed = await this.sessionManager.CloseSession(session.ID, systemUser, provider, closeReason);
|
|
193
|
+
if (closed) {
|
|
194
|
+
closedCount++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (page.length < SWEEP_PAGE_SIZE) {
|
|
198
|
+
break; // partial page => end of data
|
|
199
|
+
}
|
|
200
|
+
afterKey = CompositeKey.FromID(page[page.length - 1].ID);
|
|
201
|
+
}
|
|
202
|
+
return closedCount;
|
|
203
|
+
}
|
|
204
|
+
/** Fetch one keyset page of matching sessions, or null on a load failure (logged). */
|
|
205
|
+
async fetchPage(filter, afterKey, provider, systemUser) {
|
|
206
|
+
const params = {
|
|
207
|
+
EntityName: SESSION_ENTITY,
|
|
208
|
+
ExtraFilter: filter,
|
|
209
|
+
AfterKey: afterKey,
|
|
210
|
+
MaxRows: SWEEP_PAGE_SIZE,
|
|
211
|
+
ResultType: 'entity_object',
|
|
212
|
+
};
|
|
213
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
214
|
+
const result = await rv.RunView(params, systemUser);
|
|
215
|
+
if (!result.Success) {
|
|
216
|
+
LogError(`SessionJanitor sweep load failed: ${result.ErrorMessage}`);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return result.Results;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=SessionJanitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionJanitor.js","sourceRoot":"","sources":["../../src/agentSessions/SessionJanitor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
|
|
3
|
+
/**
|
|
4
|
+
* Inputs for {@link SessionManager.CreateSession}. `conversationID`/`lastSessionID`/`config`
|
|
5
|
+
* are optional — when no `conversationID` is supplied a fresh Conversation is created and linked.
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateSessionInput {
|
|
8
|
+
/** The agent the session will run. Authorized via `CanRun` before any row is written. */
|
|
9
|
+
agentID: string;
|
|
10
|
+
/** The owning user. Persisted as `AIAgentSession.UserID`; enforced on every inbound envelope. */
|
|
11
|
+
userID: string;
|
|
12
|
+
/** Existing conversation to attach to. If omitted, a new one is created and linked. */
|
|
13
|
+
conversationID?: string;
|
|
14
|
+
/** Prior (closed) session being resumed — chained via `LastSessionID`. */
|
|
15
|
+
lastSessionID?: string;
|
|
16
|
+
/** Free-form, low-traffic session state. Serialized verbatim into `AIAgentSession.Config`. */
|
|
17
|
+
config?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Why a session was closed — persisted to `AIAgentSession.CloseReason` alongside the
|
|
21
|
+
* `Status = 'Closed'` transition so the admin dashboards can distinguish a user hanging up
|
|
22
|
+
* (`Explicit`) from janitor reconciliation (`Janitor`), a graceful host drain (`Shutdown`),
|
|
23
|
+
* or a failure-path teardown (`Error`). `NULL` in the column means "legacy/unknown" — rows
|
|
24
|
+
* closed before this column existed.
|
|
25
|
+
*/
|
|
26
|
+
export type SessionCloseReason = 'Explicit' | 'Janitor' | 'Shutdown' | 'Error';
|
|
27
|
+
/**
|
|
28
|
+
* Thrown by {@link SessionManager.CreateSession} when the caller lacks `CanRun` on the target
|
|
29
|
+
* agent. No session row is created — denial happens before any write. Distinct error type so the
|
|
30
|
+
* resolver layer can map it to an authorization-shaped GraphQL error rather than a generic failure.
|
|
31
|
+
*/
|
|
32
|
+
export declare class SessionAuthorizationError extends Error {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Server-side lifecycle manager for `AIAgentSession` **records** (not the media/socket transport —
|
|
37
|
+
* that arrives in P5). Stateless with respect to the caller: every method takes the request's
|
|
38
|
+
* `contextUser` and request-scoped `IMetadataProvider` so all entity operations run under the
|
|
39
|
+
* correct identity/provider and never pin the process-global provider.
|
|
40
|
+
*
|
|
41
|
+
* The only in-memory state it holds is a small per-session last-heartbeat-write timestamp map used
|
|
42
|
+
* purely to coalesce {@link Heartbeat} writes; it carries no user or provider state across calls.
|
|
43
|
+
*
|
|
44
|
+
* Responsibilities:
|
|
45
|
+
* - **Create** — authorize (`CanRun`), optionally mint a Conversation, persist an `Active` session.
|
|
46
|
+
* - **Close** — terminal transition; disconnect channels; idempotent.
|
|
47
|
+
* - **Heartbeat** — coalesced `LastActiveAt` bump; reactivates `Idle → Active`.
|
|
48
|
+
* - **MarkIdle** — `Active → Idle` transition when the last channel goes quiet.
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* P5 will extend {@link CloseSession} to also abort any in-flight `AIAgentRun` (via its cancellation
|
|
52
|
+
* token) and call `ClientToolRequestManager.ClearSession(...)`; those resources don't exist yet at
|
|
53
|
+
* this layer, so close here only reconciles the durable record + channel rows.
|
|
54
|
+
*/
|
|
55
|
+
export declare class SessionManager {
|
|
56
|
+
/** session ID (lowercased) → epoch ms of its last persisted `LastActiveAt` write. */
|
|
57
|
+
private heartbeatLastWrite;
|
|
58
|
+
/**
|
|
59
|
+
* Authorize and create a new session. Flow:
|
|
60
|
+
* 1. `CanRun` check on the agent — denied → {@link SessionAuthorizationError}, no row written.
|
|
61
|
+
* 2. Resolve the conversation (use the supplied one, or create+link a fresh Conversation).
|
|
62
|
+
* 3. Persist the `AIAgentSession` as `Active`, stamped with this host and `LastActiveAt = now`.
|
|
63
|
+
*
|
|
64
|
+
* @returns the saved {@link MJAIAgentSessionEntity}.
|
|
65
|
+
* @throws {SessionAuthorizationError} when the user may not run the agent.
|
|
66
|
+
* @throws {Error} when conversation creation or session save fails.
|
|
67
|
+
*/
|
|
68
|
+
CreateSession(input: CreateSessionInput, contextUser: UserInfo, provider: IMetadataProvider): Promise<MJAIAgentSessionEntity>;
|
|
69
|
+
/**
|
|
70
|
+
* Close a session (terminal). Sets `Status = 'Closed'` + `ClosedAt = now` + `CloseReason`, then
|
|
71
|
+
* disconnects all of its channel rows. Idempotent: closing an already-`Closed` session is a
|
|
72
|
+
* no-op that returns `true` (and never overwrites the original `CloseReason`). A
|
|
73
|
+
* missing/unloadable session returns `false`.
|
|
74
|
+
*
|
|
75
|
+
* @param closeReason Why the session is being closed. **Defaults to `'Explicit'`** so existing
|
|
76
|
+
* call sites (the user-initiated close mutations) stamp the common case without modification;
|
|
77
|
+
* background callers (janitor sweeps, shutdown drain, error teardowns) must pass their reason
|
|
78
|
+
* explicitly. `NULL` rows remain only for legacy data closed before the column existed.
|
|
79
|
+
*
|
|
80
|
+
* @remarks P5 adds in-flight-run abort + `ClientToolRequestManager.ClearSession` here.
|
|
81
|
+
*/
|
|
82
|
+
CloseSession(agentSessionID: string, contextUser: UserInfo, provider: IMetadataProvider, closeReason?: SessionCloseReason): Promise<boolean>;
|
|
83
|
+
/**
|
|
84
|
+
* Notifies the server-side channel-plugin host that a session started, so it can resolve the
|
|
85
|
+
* ACTIVE `MJ: AI Agent Channels` rows' `ServerPluginClass` plugins (one fresh instance per
|
|
86
|
+
* channel, per session) and fire their start hooks. Strictly best-effort: the host itself
|
|
87
|
+
* never throws, and this guard makes session creation immune to any plugin-layer surprise.
|
|
88
|
+
*/
|
|
89
|
+
private notifyChannelPluginsSessionStarted;
|
|
90
|
+
/**
|
|
91
|
+
* Notifies the server-side channel-plugin host that a session closed (fires each plugin's
|
|
92
|
+
* `OnSessionClosed` hook and schedules disposal). Invoked from EVERY close provenance —
|
|
93
|
+
* explicit hang-up, janitor sweeps, shutdown drain, and error teardowns all funnel through
|
|
94
|
+
* {@link CloseSession}. Strictly best-effort, same posture as the start notification.
|
|
95
|
+
*/
|
|
96
|
+
private notifyChannelPluginsSessionClosed;
|
|
97
|
+
/**
|
|
98
|
+
* Finalizes the co-agent observability runs a realtime voice session stored in its `Config_`
|
|
99
|
+
* (see `RealtimeClientSessionResolver`). No-op when the config carries no run ids. Tolerant: a
|
|
100
|
+
* malformed config or a finalize failure is swallowed so it can never break session close.
|
|
101
|
+
*/
|
|
102
|
+
private finalizeObservabilityRuns;
|
|
103
|
+
/** Parses the session's `Config_` for observability run ids; returns an empty config on any error. */
|
|
104
|
+
private parseSessionRunConfig;
|
|
105
|
+
/**
|
|
106
|
+
* Record activity on a session. Coalesces persisted writes so at most one `LastActiveAt` update
|
|
107
|
+
* lands per session per {@link HEARTBEAT_MIN_WRITE_INTERVAL_MS}; intervening calls return `true`
|
|
108
|
+
* without touching the DB. Reactivates an `Idle` session to `Active`. A `Closed` session is not
|
|
109
|
+
* reactivated (returns `false`).
|
|
110
|
+
*/
|
|
111
|
+
Heartbeat(agentSessionID: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<boolean>;
|
|
112
|
+
/**
|
|
113
|
+
* Transition an `Active` session to `Idle` (e.g. when its last channel disconnects). No-op when
|
|
114
|
+
* the session is already `Idle` or `Closed`. Returns `true` on success / no-op-needed.
|
|
115
|
+
*/
|
|
116
|
+
MarkIdle(agentSessionID: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<boolean>;
|
|
117
|
+
/** Throws {@link SessionAuthorizationError} unless the user has `CanRun` on the agent. */
|
|
118
|
+
private assertCanRun;
|
|
119
|
+
/** Creates and links a new Conversation for the session, returning its ID. */
|
|
120
|
+
private createConversation;
|
|
121
|
+
/** Persists the new session row as `Active` and returns the saved entity. */
|
|
122
|
+
private persistNewSession;
|
|
123
|
+
/** Loads a session entity by ID, returning null when it cannot be loaded. */
|
|
124
|
+
private loadSession;
|
|
125
|
+
/** Sets `Closed` + `ClosedAt` + `CloseReason` and saves. Returns the save result. */
|
|
126
|
+
private markSessionClosed;
|
|
127
|
+
/** Marks every channel row of the session `Disconnected` (with `DisconnectedAt`), if not already. */
|
|
128
|
+
private disconnectChannels;
|
|
129
|
+
/** True once the per-session coalescing window has elapsed since the last persisted write. */
|
|
130
|
+
private heartbeatWriteDue;
|
|
131
|
+
/**
|
|
132
|
+
* Whether this heartbeat must write regardless of the coalescing window. We force a write the
|
|
133
|
+
* first time we see a session (no cached timestamp) so an `Idle → Active` reactivation is never
|
|
134
|
+
* swallowed by coalescing.
|
|
135
|
+
*/
|
|
136
|
+
private shouldForceWrite;
|
|
137
|
+
/** Loads, bumps `LastActiveAt`, reactivates `Idle → Active`, saves, and records the write time. */
|
|
138
|
+
private persistHeartbeat;
|
|
139
|
+
/** Saves an entity, logging `CompleteMessage` on failure. Returns the boolean save result. */
|
|
140
|
+
private saveOrLog;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=SessionManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["../../src/agentSessions/SessionManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAqB,MAAM,sBAAsB,CAAC;AACtF,OAAO,EACH,sBAAsB,EAGzB,MAAM,+BAA+B,CAAC;AAsBvC;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAC/B,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;AAE/E;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI9B;AASD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,cAAc;IACvB,qFAAqF;IACrF,OAAO,CAAC,kBAAkB,CAA6B;IAEvD;;;;;;;;;OASG;IACU,aAAa,CACtB,KAAK,EAAE,kBAAkB,EACzB,WAAW,EAAE,QAAQ,EACrB,QAAQ,EAAE,iBAAiB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAWlC;;;;;;;;;;;;OAYG;IACU,YAAY,CACrB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,QAAQ,EACrB,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,GAAE,kBAA+B,GAC7C,OAAO,CAAC,OAAO,CAAC;IAyBnB;;;;;OAKG;YACW,kCAAkC;IAqBhD;;;;;OAKG;YACW,iCAAiC;IAW/C;;;;OAIG;YACW,yBAAyB;IAuBvC,sGAAsG;IACtG,OAAO,CAAC,qBAAqB;IAgB7B;;;;;OAKG;IACU,SAAS,CAClB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,QAAQ,EACrB,QAAQ,EAAE,iBAAiB,GAC5B,OAAO,CAAC,OAAO,CAAC;IASnB;;;OAGG;IACU,QAAQ,CACjB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,QAAQ,EACrB,QAAQ,EAAE,iBAAiB,GAC5B,OAAO,CAAC,OAAO,CAAC;IAcnB,0FAA0F;YAC5E,YAAY;IAS1B,8EAA8E;YAChE,kBAAkB;IAmBhC,6EAA6E;YAC/D,iBAAiB;IA0B/B,6EAA6E;YAC/D,WAAW;IAUzB,qFAAqF;YACvE,iBAAiB;IAU/B,qGAAqG;YACvF,kBAAkB;IAwBhC,8FAA8F;IAC9F,OAAO,CAAC,iBAAiB;IAKzB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAIxB,mGAAmG;YACrF,gBAAgB;IAoB9B,8FAA8F;YAChF,SAAS;CAU1B"}
|