@undefineds.co/xpod 0.3.46 → 0.3.49

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 (106) hide show
  1. package/bin/xpod.js +0 -0
  2. package/dist/api/chatkit/pod-store.d.ts +16 -17
  3. package/dist/api/chatkit/pod-store.js +299 -231
  4. package/dist/api/chatkit/pod-store.js.map +1 -1
  5. package/dist/api/chatkit/schema.d.ts +2 -2
  6. package/dist/api/chatkit/service.js +13 -11
  7. package/dist/api/chatkit/service.js.map +1 -1
  8. package/dist/api/chatkit/store.d.ts +1 -0
  9. package/dist/api/chatkit/store.js +23 -11
  10. package/dist/api/chatkit/store.js.map +1 -1
  11. package/dist/api/chatkit/types.d.ts +17 -10
  12. package/dist/api/chatkit/types.js +97 -14
  13. package/dist/api/chatkit/types.js.map +1 -1
  14. package/dist/api/container/common.js +16 -2
  15. package/dist/api/container/common.js.map +1 -1
  16. package/dist/api/container/routes.js +3 -0
  17. package/dist/api/container/routes.js.map +1 -1
  18. package/dist/api/container/types.d.ts +3 -0
  19. package/dist/api/container/types.js.map +1 -1
  20. package/dist/api/handlers/ChatKitV1Handler.js +1 -2
  21. package/dist/api/handlers/ChatKitV1Handler.js.map +1 -1
  22. package/dist/api/handlers/CoordinationHandler.d.ts +6 -0
  23. package/dist/api/handlers/CoordinationHandler.js +115 -0
  24. package/dist/api/handlers/CoordinationHandler.js.map +1 -0
  25. package/dist/api/handlers/MatrixHandler.d.ts +11 -0
  26. package/dist/api/handlers/MatrixHandler.js +120 -2
  27. package/dist/api/handlers/MatrixHandler.js.map +1 -1
  28. package/dist/api/handlers/RunHandler.js +33 -15
  29. package/dist/api/handlers/RunHandler.js.map +1 -1
  30. package/dist/api/handlers/index.d.ts +1 -0
  31. package/dist/api/handlers/index.js +1 -0
  32. package/dist/api/handlers/index.js.map +1 -1
  33. package/dist/api/index.d.ts +1 -0
  34. package/dist/api/index.js +1 -0
  35. package/dist/api/index.js.map +1 -1
  36. package/dist/api/matrix/PodMatrixStore.d.ts +25 -1
  37. package/dist/api/matrix/PodMatrixStore.js +243 -38
  38. package/dist/api/matrix/PodMatrixStore.js.map +1 -1
  39. package/dist/api/matrix/index.d.ts +1 -1
  40. package/dist/api/matrix/index.js.map +1 -1
  41. package/dist/api/matrix/types.d.ts +23 -2
  42. package/dist/api/matrix/types.js.map +1 -1
  43. package/dist/api/protocol-metadata.d.ts +4 -0
  44. package/dist/api/protocol-metadata.js +54 -0
  45. package/dist/api/protocol-metadata.js.map +1 -0
  46. package/dist/api/reconciler/ClientReconcilerCoordinator.d.ts +42 -0
  47. package/dist/api/reconciler/ClientReconcilerCoordinator.js +250 -0
  48. package/dist/api/reconciler/ClientReconcilerCoordinator.js.map +1 -0
  49. package/dist/api/reconciler/ClientReconcilerCoordinator.jsonld +186 -0
  50. package/dist/api/reconciler/ServerGroupReconcilerService.d.ts +39 -0
  51. package/dist/api/reconciler/ServerGroupReconcilerService.js +91 -0
  52. package/dist/api/reconciler/ServerGroupReconcilerService.js.map +1 -0
  53. package/dist/api/reconciler/ServerGroupReconcilerService.jsonld +146 -0
  54. package/dist/api/reconciler/WakeAgentQueue.d.ts +23 -0
  55. package/dist/api/reconciler/WakeAgentQueue.js +123 -0
  56. package/dist/api/reconciler/WakeAgentQueue.js.map +1 -0
  57. package/dist/api/reconciler/WakeAgentQueue.jsonld +91 -0
  58. package/dist/api/reconciler/coordination.d.ts +61 -0
  59. package/dist/api/reconciler/coordination.js +109 -0
  60. package/dist/api/reconciler/coordination.js.map +1 -0
  61. package/dist/api/reconciler/coordination.jsonld +186 -0
  62. package/dist/api/reconciler/index.d.ts +4 -0
  63. package/dist/api/reconciler/index.js +21 -0
  64. package/dist/api/reconciler/index.js.map +1 -0
  65. package/dist/api/runs/ManagedRunWorker.js +0 -5
  66. package/dist/api/runs/ManagedRunWorker.js.map +1 -1
  67. package/dist/api/runs/RunStateCenter.d.ts +1 -1
  68. package/dist/api/runs/RunStateCenter.js +12 -28
  69. package/dist/api/runs/RunStateCenter.js.map +1 -1
  70. package/dist/api/runs/store.d.ts +12 -15
  71. package/dist/api/runs/store.js +24 -15
  72. package/dist/api/runs/store.js.map +1 -1
  73. package/dist/api/tasks/TaskMaterializer.d.ts +1 -0
  74. package/dist/api/tasks/TaskMaterializer.js +10 -13
  75. package/dist/api/tasks/TaskMaterializer.js.map +1 -1
  76. package/dist/api/tasks/TaskService.d.ts +0 -2
  77. package/dist/api/tasks/TaskService.js +6 -16
  78. package/dist/api/tasks/TaskService.js.map +1 -1
  79. package/dist/api/tasks/schema.d.ts +0 -1
  80. package/dist/api/tasks/store.d.ts +0 -2
  81. package/dist/api/tasks/store.js.map +1 -1
  82. package/dist/cli/commands/auth.d.ts +1 -1
  83. package/dist/cli/commands/auth.js +4 -5
  84. package/dist/cli/commands/auth.js.map +1 -1
  85. package/dist/cli/commands/backup.js +1 -1
  86. package/dist/cli/commands/backup.js.map +1 -1
  87. package/dist/cli/commands/login.js +1 -1
  88. package/dist/cli/commands/login.js.map +1 -1
  89. package/dist/cli/commands/pod.js +1 -1
  90. package/dist/cli/commands/pod.js.map +1 -1
  91. package/dist/cli/lib/auth-helper.d.ts +5 -3
  92. package/dist/cli/lib/auth-helper.js +5 -3
  93. package/dist/cli/lib/auth-helper.js.map +1 -1
  94. package/dist/cli/lib/credentials-store.d.ts +22 -4
  95. package/dist/cli/lib/credentials-store.js +68 -51
  96. package/dist/cli/lib/credentials-store.js.map +1 -1
  97. package/dist/components/components.jsonld +5 -1
  98. package/dist/components/context.jsonld +103 -0
  99. package/dist/index.d.ts +1 -0
  100. package/dist/index.js +15 -0
  101. package/dist/index.js.map +1 -1
  102. package/dist/provision/LocalPodProvisioningService.d.ts +1 -0
  103. package/dist/provision/LocalPodProvisioningService.js +9 -0
  104. package/dist/provision/LocalPodProvisioningService.js.map +1 -1
  105. package/dist/provision/LocalPodProvisioningService.jsonld +4 -0
  106. package/package.json +2 -2
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ClientReconcilerCoordinator = exports.DEFAULT_CLIENT_RECONCILER_LEASE_TTL_MS = exports.DEFAULT_CLIENT_RECONCILER_HEARTBEAT_TTL_MS = void 0;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const ioredis_1 = __importDefault(require("ioredis"));
9
+ const global_logger_factory_1 = require("global-logger-factory");
10
+ const coordination_1 = require("./coordination");
11
+ const RedisClientLifecycle_1 = require("../../storage/redis/RedisClientLifecycle");
12
+ exports.DEFAULT_CLIENT_RECONCILER_HEARTBEAT_TTL_MS = 45_000;
13
+ exports.DEFAULT_CLIENT_RECONCILER_LEASE_TTL_MS = 30_000;
14
+ class ClientReconcilerCoordinator {
15
+ constructor(options = {}) {
16
+ this.now = options.now ?? (() => new Date());
17
+ this.heartbeatTtlMs = options.heartbeatTtlMs ?? exports.DEFAULT_CLIENT_RECONCILER_HEARTBEAT_TTL_MS;
18
+ this.leaseTtlMs = options.leaseTtlMs ?? exports.DEFAULT_CLIENT_RECONCILER_LEASE_TTL_MS;
19
+ this.backend = options.redisUrl
20
+ ? new RedisClientReconcilerCoordinatorBackend({
21
+ redisUrl: options.redisUrl,
22
+ namespace: options.namespace,
23
+ heartbeatTtlMs: this.heartbeatTtlMs,
24
+ })
25
+ : new InMemoryClientReconcilerCoordinatorBackend({
26
+ heartbeatTtlMs: this.heartbeatTtlMs,
27
+ });
28
+ }
29
+ async upsertClientCapability(input) {
30
+ const now = this.now();
31
+ const capability = {
32
+ clientId: requireNonEmptyString(input.clientId, 'clientId'),
33
+ kind: normalizeClientKind(input.kind),
34
+ user: requireNonEmptyString(input.user, 'user'),
35
+ canCoordinateClientOwnedThread: input.canCoordinateClientOwnedThread ?? false,
36
+ canRunAgent: input.canRunAgent ?? false,
37
+ workspaces: sanitizeWorkspaces(input.workspaces),
38
+ heartbeatAt: parseDateOrNow(input.heartbeatAt, now).toISOString(),
39
+ };
40
+ await this.backend.sweepExpired(now);
41
+ return this.backend.upsertClientCapability(capability);
42
+ }
43
+ async listClientCapabilities(user) {
44
+ const now = this.now();
45
+ await this.backend.sweepExpired(now);
46
+ return this.backend.listClientCapabilities(requireNonEmptyString(user, 'user'));
47
+ }
48
+ async activate(input) {
49
+ const now = this.now();
50
+ const thread = requireNonEmptyString(input.thread, 'thread');
51
+ const ownerUser = requireNonEmptyString(input.ownerUser, 'ownerUser');
52
+ await this.backend.sweepExpired(now);
53
+ const currentLease = await this.backend.getLease(thread);
54
+ const clients = await this.backend.listClientCapabilities(ownerUser);
55
+ const lease = (0, coordination_1.activateClientReconciler)({
56
+ thread,
57
+ ownerUser,
58
+ clients,
59
+ currentLease,
60
+ now,
61
+ heartbeatTtlMs: this.heartbeatTtlMs,
62
+ leaseTtlMs: this.leaseTtlMs,
63
+ fencingToken: (0, node_crypto_1.randomUUID)(),
64
+ });
65
+ if (!lease) {
66
+ return undefined;
67
+ }
68
+ return this.backend.saveLease(lease, this.leaseTtlMs);
69
+ }
70
+ async getLease(thread) {
71
+ const now = this.now();
72
+ await this.backend.sweepExpired(now);
73
+ const lease = await this.backend.getLease(requireNonEmptyString(thread, 'thread'));
74
+ return (0, coordination_1.isClientReconcilerLeaseActive)(lease, now) ? lease : undefined;
75
+ }
76
+ async releaseLease(input) {
77
+ await this.backend.sweepExpired(this.now());
78
+ return this.backend.releaseLease({
79
+ thread: requireNonEmptyString(input.thread, 'thread'),
80
+ ownerUser: requireNonEmptyString(input.ownerUser, 'ownerUser'),
81
+ clientId: requireNonEmptyString(input.clientId, 'clientId'),
82
+ });
83
+ }
84
+ async close() {
85
+ await this.backend.close?.();
86
+ }
87
+ }
88
+ exports.ClientReconcilerCoordinator = ClientReconcilerCoordinator;
89
+ class InMemoryClientReconcilerCoordinatorBackend {
90
+ constructor(options) {
91
+ this.clients = new Map();
92
+ this.leases = new Map();
93
+ this.heartbeatTtlMs = options.heartbeatTtlMs;
94
+ }
95
+ async upsertClientCapability(capability) {
96
+ this.clients.set(clientKey(capability.user, capability.clientId), { ...capability, workspaces: [...capability.workspaces] });
97
+ return { ...capability, workspaces: [...capability.workspaces] };
98
+ }
99
+ async listClientCapabilities(user) {
100
+ return Array.from(this.clients.values())
101
+ .filter((client) => client.user === user)
102
+ .map((client) => ({ ...client, workspaces: [...client.workspaces] }));
103
+ }
104
+ async getLease(thread) {
105
+ const lease = this.leases.get(thread);
106
+ return lease ? { ...lease } : undefined;
107
+ }
108
+ async saveLease(lease) {
109
+ this.leases.set(lease.thread, { ...lease });
110
+ return { ...lease };
111
+ }
112
+ async releaseLease(input) {
113
+ const lease = this.leases.get(input.thread);
114
+ if (!lease || lease.ownerUser !== input.ownerUser || lease.ownerClientId !== input.clientId) {
115
+ return false;
116
+ }
117
+ return this.leases.delete(input.thread);
118
+ }
119
+ async sweepExpired(now) {
120
+ for (const [key, client] of this.clients) {
121
+ const heartbeatAt = Date.parse(client.heartbeatAt);
122
+ if (!Number.isFinite(heartbeatAt) || now.getTime() - heartbeatAt > this.heartbeatTtlMs) {
123
+ this.clients.delete(key);
124
+ }
125
+ }
126
+ for (const [thread, lease] of this.leases) {
127
+ if (!(0, coordination_1.isClientReconcilerLeaseActive)(lease, now)) {
128
+ this.leases.delete(thread);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ class RedisClientReconcilerCoordinatorBackend {
134
+ constructor(options) {
135
+ this.logger = (0, global_logger_factory_1.getLoggerFor)(this);
136
+ this.shuttingDown = false;
137
+ this.namespace = options.namespace ?? 'xpod:coordination:';
138
+ this.heartbeatTtlMs = options.heartbeatTtlMs;
139
+ this.redis = new ioredis_1.default(options.redisUrl, { lazyConnect: false });
140
+ (0, RedisClientLifecycle_1.attachRedisClientErrorHandler)(this.redis, {
141
+ logger: this.logger,
142
+ label: 'ClientReconcilerCoordinator',
143
+ isShuttingDown: () => this.shuttingDown,
144
+ });
145
+ }
146
+ async upsertClientCapability(capability) {
147
+ await this.redis.set(this.clientStorageKey(capability.user, capability.clientId), JSON.stringify(capability), 'PX', this.heartbeatTtlMs);
148
+ return { ...capability, workspaces: [...capability.workspaces] };
149
+ }
150
+ async listClientCapabilities(user) {
151
+ const keys = await this.scanKeys(`${this.clientStoragePrefix(user)}*`);
152
+ if (keys.length === 0) {
153
+ return [];
154
+ }
155
+ const raws = await this.redis.mget(keys);
156
+ return raws
157
+ .map((raw) => parseJson(raw))
158
+ .filter((client) => Boolean(client && client.user === user));
159
+ }
160
+ async getLease(thread) {
161
+ return parseJson(await this.redis.get(this.leaseStorageKey(thread)));
162
+ }
163
+ async saveLease(lease, ttlMs) {
164
+ await this.redis.set(this.leaseStorageKey(lease.thread), JSON.stringify(lease), 'PX', ttlMs);
165
+ return { ...lease };
166
+ }
167
+ async releaseLease(input) {
168
+ const key = this.leaseStorageKey(input.thread);
169
+ const released = await this.redis.eval(`local raw = redis.call('GET', KEYS[1])
170
+ if not raw then return 0 end
171
+ local value = cjson.decode(raw)
172
+ if value['ownerUser'] == ARGV[1] and value['ownerClientId'] == ARGV[2] then
173
+ return redis.call('DEL', KEYS[1])
174
+ end
175
+ return 0`, 1, key, input.ownerUser, input.clientId);
176
+ return released === 1;
177
+ }
178
+ async sweepExpired(_now) {
179
+ // Redis key TTL owns expiry. No active sweep is required.
180
+ }
181
+ async close() {
182
+ this.shuttingDown = true;
183
+ await (0, RedisClientLifecycle_1.closeRedisClient)(this.redis, {
184
+ logger: this.logger,
185
+ label: 'ClientReconcilerCoordinator',
186
+ });
187
+ }
188
+ clientStoragePrefix(user) {
189
+ return `${this.namespace}client:${hash(user)}:`;
190
+ }
191
+ clientStorageKey(user, clientId) {
192
+ return `${this.clientStoragePrefix(user)}${encodeURIComponent(clientId)}`;
193
+ }
194
+ leaseStorageKey(thread) {
195
+ return `${this.namespace}lease:${hash(thread)}`;
196
+ }
197
+ async scanKeys(match) {
198
+ const keys = [];
199
+ let cursor = '0';
200
+ do {
201
+ const [nextCursor, batch] = await this.redis.scan(cursor, 'MATCH', match, 'COUNT', 100);
202
+ cursor = nextCursor;
203
+ keys.push(...batch);
204
+ } while (cursor !== '0');
205
+ return keys;
206
+ }
207
+ }
208
+ function normalizeClientKind(kind) {
209
+ if (kind === 'cli' || kind === 'desktop' || kind === 'mobile' || kind === 'web') {
210
+ return kind;
211
+ }
212
+ throw new Error('kind must be one of cli, desktop, mobile, web');
213
+ }
214
+ function sanitizeWorkspaces(workspaces) {
215
+ if (!Array.isArray(workspaces)) {
216
+ return [];
217
+ }
218
+ return Array.from(new Set(workspaces.filter((ref) => typeof ref === 'string' && ref.length > 0)));
219
+ }
220
+ function parseDateOrNow(value, now) {
221
+ if (typeof value !== 'string') {
222
+ return now;
223
+ }
224
+ const parsed = Date.parse(value);
225
+ return Number.isFinite(parsed) ? new Date(parsed) : now;
226
+ }
227
+ function requireNonEmptyString(value, field) {
228
+ if (typeof value !== 'string' || value.trim().length === 0) {
229
+ throw new Error(`${field} is required`);
230
+ }
231
+ return value.trim();
232
+ }
233
+ function clientKey(user, clientId) {
234
+ return `${user}\u0000${clientId}`;
235
+ }
236
+ function hash(value) {
237
+ return (0, node_crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, 32);
238
+ }
239
+ function parseJson(raw) {
240
+ if (!raw) {
241
+ return undefined;
242
+ }
243
+ try {
244
+ return JSON.parse(raw);
245
+ }
246
+ catch {
247
+ return undefined;
248
+ }
249
+ }
250
+ //# sourceMappingURL=ClientReconcilerCoordinator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientReconcilerCoordinator.js","sourceRoot":"","sources":["../../../src/api/reconciler/ClientReconcilerCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport Redis from 'ioredis';\nimport { getLoggerFor } from 'global-logger-factory';\nimport {\n activateClientReconciler,\n isClientReconcilerLeaseActive,\n type ClientCapability,\n type ClientKind,\n type ClientReconcilerLease,\n} from './coordination';\nimport {\n attachRedisClientErrorHandler,\n closeRedisClient,\n} from '../../storage/redis/RedisClientLifecycle';\n\nexport const DEFAULT_CLIENT_RECONCILER_HEARTBEAT_TTL_MS = 45_000;\nexport const DEFAULT_CLIENT_RECONCILER_LEASE_TTL_MS = 30_000;\n\nexport interface ClientReconcilerCoordinatorOptions {\n redisUrl?: string;\n now?: () => Date;\n heartbeatTtlMs?: number;\n leaseTtlMs?: number;\n namespace?: string;\n}\n\nexport interface UpsertClientCapabilityInput {\n clientId: string;\n kind: ClientKind;\n user: string;\n canCoordinateClientOwnedThread?: boolean;\n canRunAgent?: boolean;\n workspaces?: string[];\n heartbeatAt?: string;\n}\n\nexport interface ActivateClientReconcilerLeaseInput {\n thread: string;\n ownerUser: string;\n requesterClientId?: string;\n}\n\nexport interface ReleaseClientReconcilerLeaseInput {\n thread: string;\n ownerUser: string;\n clientId: string;\n}\n\ninterface ClientReconcilerCoordinatorBackend {\n upsertClientCapability(capability: ClientCapability): Promise<ClientCapability>;\n listClientCapabilities(user: string): Promise<ClientCapability[]>;\n getLease(thread: string): Promise<ClientReconcilerLease | undefined>;\n saveLease(lease: ClientReconcilerLease, ttlMs: number): Promise<ClientReconcilerLease>;\n releaseLease(input: ReleaseClientReconcilerLeaseInput): Promise<boolean>;\n sweepExpired(now: Date): Promise<void>;\n close?(): Promise<void>;\n}\n\nexport class ClientReconcilerCoordinator {\n private readonly now: () => Date;\n private readonly heartbeatTtlMs: number;\n private readonly leaseTtlMs: number;\n private readonly backend: ClientReconcilerCoordinatorBackend;\n\n public constructor(options: ClientReconcilerCoordinatorOptions = {}) {\n this.now = options.now ?? (() => new Date());\n this.heartbeatTtlMs = options.heartbeatTtlMs ?? DEFAULT_CLIENT_RECONCILER_HEARTBEAT_TTL_MS;\n this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_CLIENT_RECONCILER_LEASE_TTL_MS;\n this.backend = options.redisUrl\n ? new RedisClientReconcilerCoordinatorBackend({\n redisUrl: options.redisUrl,\n namespace: options.namespace,\n heartbeatTtlMs: this.heartbeatTtlMs,\n })\n : new InMemoryClientReconcilerCoordinatorBackend({\n heartbeatTtlMs: this.heartbeatTtlMs,\n });\n }\n\n public async upsertClientCapability(input: UpsertClientCapabilityInput): Promise<ClientCapability> {\n const now = this.now();\n const capability: ClientCapability = {\n clientId: requireNonEmptyString(input.clientId, 'clientId'),\n kind: normalizeClientKind(input.kind),\n user: requireNonEmptyString(input.user, 'user'),\n canCoordinateClientOwnedThread: input.canCoordinateClientOwnedThread ?? false,\n canRunAgent: input.canRunAgent ?? false,\n workspaces: sanitizeWorkspaces(input.workspaces),\n heartbeatAt: parseDateOrNow(input.heartbeatAt, now).toISOString(),\n };\n await this.backend.sweepExpired(now);\n return this.backend.upsertClientCapability(capability);\n }\n\n public async listClientCapabilities(user: string): Promise<ClientCapability[]> {\n const now = this.now();\n await this.backend.sweepExpired(now);\n return this.backend.listClientCapabilities(requireNonEmptyString(user, 'user'));\n }\n\n public async activate(input: ActivateClientReconcilerLeaseInput): Promise<ClientReconcilerLease | undefined> {\n const now = this.now();\n const thread = requireNonEmptyString(input.thread, 'thread');\n const ownerUser = requireNonEmptyString(input.ownerUser, 'ownerUser');\n await this.backend.sweepExpired(now);\n\n const currentLease = await this.backend.getLease(thread);\n const clients = await this.backend.listClientCapabilities(ownerUser);\n const lease = activateClientReconciler({\n thread,\n ownerUser,\n clients,\n currentLease,\n now,\n heartbeatTtlMs: this.heartbeatTtlMs,\n leaseTtlMs: this.leaseTtlMs,\n fencingToken: randomUUID(),\n });\n\n if (!lease) {\n return undefined;\n }\n return this.backend.saveLease(lease, this.leaseTtlMs);\n }\n\n public async getLease(thread: string): Promise<ClientReconcilerLease | undefined> {\n const now = this.now();\n await this.backend.sweepExpired(now);\n const lease = await this.backend.getLease(requireNonEmptyString(thread, 'thread'));\n return isClientReconcilerLeaseActive(lease, now) ? lease : undefined;\n }\n\n public async releaseLease(input: ReleaseClientReconcilerLeaseInput): Promise<boolean> {\n await this.backend.sweepExpired(this.now());\n return this.backend.releaseLease({\n thread: requireNonEmptyString(input.thread, 'thread'),\n ownerUser: requireNonEmptyString(input.ownerUser, 'ownerUser'),\n clientId: requireNonEmptyString(input.clientId, 'clientId'),\n });\n }\n\n public async close(): Promise<void> {\n await this.backend.close?.();\n }\n}\n\nclass InMemoryClientReconcilerCoordinatorBackend implements ClientReconcilerCoordinatorBackend {\n private readonly heartbeatTtlMs: number;\n private readonly clients = new Map<string, ClientCapability>();\n private readonly leases = new Map<string, ClientReconcilerLease>();\n\n public constructor(options: { heartbeatTtlMs: number }) {\n this.heartbeatTtlMs = options.heartbeatTtlMs;\n }\n\n public async upsertClientCapability(capability: ClientCapability): Promise<ClientCapability> {\n this.clients.set(clientKey(capability.user, capability.clientId), { ...capability, workspaces: [ ...capability.workspaces ] });\n return { ...capability, workspaces: [ ...capability.workspaces ] };\n }\n\n public async listClientCapabilities(user: string): Promise<ClientCapability[]> {\n return Array.from(this.clients.values())\n .filter((client) => client.user === user)\n .map((client) => ({ ...client, workspaces: [ ...client.workspaces ] }));\n }\n\n public async getLease(thread: string): Promise<ClientReconcilerLease | undefined> {\n const lease = this.leases.get(thread);\n return lease ? { ...lease } : undefined;\n }\n\n public async saveLease(lease: ClientReconcilerLease): Promise<ClientReconcilerLease> {\n this.leases.set(lease.thread, { ...lease });\n return { ...lease };\n }\n\n public async releaseLease(input: ReleaseClientReconcilerLeaseInput): Promise<boolean> {\n const lease = this.leases.get(input.thread);\n if (!lease || lease.ownerUser !== input.ownerUser || lease.ownerClientId !== input.clientId) {\n return false;\n }\n return this.leases.delete(input.thread);\n }\n\n public async sweepExpired(now: Date): Promise<void> {\n for (const [ key, client ] of this.clients) {\n const heartbeatAt = Date.parse(client.heartbeatAt);\n if (!Number.isFinite(heartbeatAt) || now.getTime() - heartbeatAt > this.heartbeatTtlMs) {\n this.clients.delete(key);\n }\n }\n for (const [ thread, lease ] of this.leases) {\n if (!isClientReconcilerLeaseActive(lease, now)) {\n this.leases.delete(thread);\n }\n }\n }\n}\n\nclass RedisClientReconcilerCoordinatorBackend implements ClientReconcilerCoordinatorBackend {\n private readonly logger = getLoggerFor(this);\n private readonly redis: Redis;\n private readonly namespace: string;\n private readonly heartbeatTtlMs: number;\n private shuttingDown = false;\n\n public constructor(options: { redisUrl: string; namespace?: string; heartbeatTtlMs: number }) {\n this.namespace = options.namespace ?? 'xpod:coordination:';\n this.heartbeatTtlMs = options.heartbeatTtlMs;\n this.redis = new Redis(options.redisUrl, { lazyConnect: false });\n attachRedisClientErrorHandler(this.redis, {\n logger: this.logger,\n label: 'ClientReconcilerCoordinator',\n isShuttingDown: () => this.shuttingDown,\n });\n }\n\n public async upsertClientCapability(capability: ClientCapability): Promise<ClientCapability> {\n await this.redis.set(\n this.clientStorageKey(capability.user, capability.clientId),\n JSON.stringify(capability),\n 'PX',\n this.heartbeatTtlMs,\n );\n return { ...capability, workspaces: [ ...capability.workspaces ] };\n }\n\n public async listClientCapabilities(user: string): Promise<ClientCapability[]> {\n const keys = await this.scanKeys(`${this.clientStoragePrefix(user)}*`);\n if (keys.length === 0) {\n return [];\n }\n const raws = await this.redis.mget(keys);\n return raws\n .map((raw) => parseJson<ClientCapability>(raw))\n .filter((client): client is ClientCapability => Boolean(client && client.user === user));\n }\n\n public async getLease(thread: string): Promise<ClientReconcilerLease | undefined> {\n return parseJson<ClientReconcilerLease>(await this.redis.get(this.leaseStorageKey(thread)));\n }\n\n public async saveLease(lease: ClientReconcilerLease, ttlMs: number): Promise<ClientReconcilerLease> {\n await this.redis.set(this.leaseStorageKey(lease.thread), JSON.stringify(lease), 'PX', ttlMs);\n return { ...lease };\n }\n\n public async releaseLease(input: ReleaseClientReconcilerLeaseInput): Promise<boolean> {\n const key = this.leaseStorageKey(input.thread);\n const released = await this.redis.eval(\n `local raw = redis.call('GET', KEYS[1])\n if not raw then return 0 end\n local value = cjson.decode(raw)\n if value['ownerUser'] == ARGV[1] and value['ownerClientId'] == ARGV[2] then\n return redis.call('DEL', KEYS[1])\n end\n return 0`,\n 1,\n key,\n input.ownerUser,\n input.clientId,\n );\n return released === 1;\n }\n\n public async sweepExpired(_now: Date): Promise<void> {\n // Redis key TTL owns expiry. No active sweep is required.\n }\n\n public async close(): Promise<void> {\n this.shuttingDown = true;\n await closeRedisClient(this.redis, {\n logger: this.logger,\n label: 'ClientReconcilerCoordinator',\n });\n }\n\n private clientStoragePrefix(user: string): string {\n return `${this.namespace}client:${hash(user)}:`;\n }\n\n private clientStorageKey(user: string, clientId: string): string {\n return `${this.clientStoragePrefix(user)}${encodeURIComponent(clientId)}`;\n }\n\n private leaseStorageKey(thread: string): string {\n return `${this.namespace}lease:${hash(thread)}`;\n }\n\n private async scanKeys(match: string): Promise<string[]> {\n const keys: string[] = [];\n let cursor = '0';\n do {\n const [ nextCursor, batch ] = await this.redis.scan(cursor, 'MATCH', match, 'COUNT', 100);\n cursor = nextCursor;\n keys.push(...batch);\n } while (cursor !== '0');\n return keys;\n }\n}\n\nfunction normalizeClientKind(kind: unknown): ClientKind {\n if (kind === 'cli' || kind === 'desktop' || kind === 'mobile' || kind === 'web') {\n return kind;\n }\n throw new Error('kind must be one of cli, desktop, mobile, web');\n}\n\nfunction sanitizeWorkspaces(workspaces: unknown): string[] {\n if (!Array.isArray(workspaces)) {\n return [];\n }\n return Array.from(new Set(workspaces.filter((ref): ref is string => typeof ref === 'string' && ref.length > 0)));\n}\n\nfunction parseDateOrNow(value: unknown, now: Date): Date {\n if (typeof value !== 'string') {\n return now;\n }\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? new Date(parsed) : now;\n}\n\nfunction requireNonEmptyString(value: unknown, field: string): string {\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new Error(`${field} is required`);\n }\n return value.trim();\n}\n\nfunction clientKey(user: string, clientId: string): string {\n return `${user}\\u0000${clientId}`;\n}\n\nfunction hash(value: string): string {\n return createHash('sha256').update(value).digest('hex').slice(0, 32);\n}\n\nfunction parseJson<T>(raw: string | null): T | undefined {\n if (!raw) {\n return undefined;\n }\n try {\n return JSON.parse(raw) as T;\n } catch {\n return undefined;\n }\n}\n"]}
@@ -0,0 +1,186 @@
1
+ {
2
+ "@context": [
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@undefineds.co/xpod/^0.0.0/components/context.jsonld"
4
+ ],
5
+ "@id": "npmd:@undefineds.co/xpod",
6
+ "components": [
7
+ {
8
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator",
9
+ "@type": "Class",
10
+ "requireElement": "ClientReconcilerCoordinator",
11
+ "parameters": [
12
+ {
13
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator_options",
14
+ "range": {
15
+ "@type": "ParameterRangeUnion",
16
+ "parameterRangeElements": [
17
+ "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinatorOptions",
18
+ {
19
+ "@type": "ParameterRangeUndefined"
20
+ }
21
+ ]
22
+ }
23
+ }
24
+ ],
25
+ "memberFields": [
26
+ {
27
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_now",
28
+ "memberFieldName": "now"
29
+ },
30
+ {
31
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_heartbeatTtlMs",
32
+ "memberFieldName": "heartbeatTtlMs"
33
+ },
34
+ {
35
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_leaseTtlMs",
36
+ "memberFieldName": "leaseTtlMs"
37
+ },
38
+ {
39
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_backend",
40
+ "memberFieldName": "backend"
41
+ },
42
+ {
43
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_constructor",
44
+ "memberFieldName": "constructor"
45
+ },
46
+ {
47
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_upsertClientCapability",
48
+ "memberFieldName": "upsertClientCapability"
49
+ },
50
+ {
51
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_listClientCapabilities",
52
+ "memberFieldName": "listClientCapabilities"
53
+ },
54
+ {
55
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_activate",
56
+ "memberFieldName": "activate"
57
+ },
58
+ {
59
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_getLease",
60
+ "memberFieldName": "getLease"
61
+ },
62
+ {
63
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_releaseLease",
64
+ "memberFieldName": "releaseLease"
65
+ },
66
+ {
67
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator__member_close",
68
+ "memberFieldName": "close"
69
+ }
70
+ ],
71
+ "constructorArguments": [
72
+ {
73
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinator_options"
74
+ }
75
+ ]
76
+ },
77
+ {
78
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinatorOptions",
79
+ "@type": "AbstractClass",
80
+ "requireElement": "ClientReconcilerCoordinatorOptions",
81
+ "parameters": [],
82
+ "memberFields": [
83
+ {
84
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinatorOptions__member_redisUrl",
85
+ "memberFieldName": "redisUrl"
86
+ },
87
+ {
88
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinatorOptions__member_now",
89
+ "memberFieldName": "now"
90
+ },
91
+ {
92
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinatorOptions__member_heartbeatTtlMs",
93
+ "memberFieldName": "heartbeatTtlMs"
94
+ },
95
+ {
96
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinatorOptions__member_leaseTtlMs",
97
+ "memberFieldName": "leaseTtlMs"
98
+ },
99
+ {
100
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ClientReconcilerCoordinatorOptions__member_namespace",
101
+ "memberFieldName": "namespace"
102
+ }
103
+ ],
104
+ "constructorArguments": []
105
+ },
106
+ {
107
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput",
108
+ "@type": "AbstractClass",
109
+ "requireElement": "UpsertClientCapabilityInput",
110
+ "parameters": [],
111
+ "memberFields": [
112
+ {
113
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput__member_clientId",
114
+ "memberFieldName": "clientId"
115
+ },
116
+ {
117
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput__member_kind",
118
+ "memberFieldName": "kind"
119
+ },
120
+ {
121
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput__member_user",
122
+ "memberFieldName": "user"
123
+ },
124
+ {
125
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput__member_canCoordinateClientOwnedThread",
126
+ "memberFieldName": "canCoordinateClientOwnedThread"
127
+ },
128
+ {
129
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput__member_canRunAgent",
130
+ "memberFieldName": "canRunAgent"
131
+ },
132
+ {
133
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput__member_workspaces",
134
+ "memberFieldName": "workspaces"
135
+ },
136
+ {
137
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#UpsertClientCapabilityInput__member_heartbeatAt",
138
+ "memberFieldName": "heartbeatAt"
139
+ }
140
+ ],
141
+ "constructorArguments": []
142
+ },
143
+ {
144
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ActivateClientReconcilerLeaseInput",
145
+ "@type": "AbstractClass",
146
+ "requireElement": "ActivateClientReconcilerLeaseInput",
147
+ "parameters": [],
148
+ "memberFields": [
149
+ {
150
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ActivateClientReconcilerLeaseInput__member_thread",
151
+ "memberFieldName": "thread"
152
+ },
153
+ {
154
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ActivateClientReconcilerLeaseInput__member_ownerUser",
155
+ "memberFieldName": "ownerUser"
156
+ },
157
+ {
158
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ActivateClientReconcilerLeaseInput__member_requesterClientId",
159
+ "memberFieldName": "requesterClientId"
160
+ }
161
+ ],
162
+ "constructorArguments": []
163
+ },
164
+ {
165
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ReleaseClientReconcilerLeaseInput",
166
+ "@type": "AbstractClass",
167
+ "requireElement": "ReleaseClientReconcilerLeaseInput",
168
+ "parameters": [],
169
+ "memberFields": [
170
+ {
171
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ReleaseClientReconcilerLeaseInput__member_thread",
172
+ "memberFieldName": "thread"
173
+ },
174
+ {
175
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ReleaseClientReconcilerLeaseInput__member_ownerUser",
176
+ "memberFieldName": "ownerUser"
177
+ },
178
+ {
179
+ "@id": "undefineds:dist/api/reconciler/ClientReconcilerCoordinator.jsonld#ReleaseClientReconcilerLeaseInput__member_clientId",
180
+ "memberFieldName": "clientId"
181
+ }
182
+ ],
183
+ "constructorArguments": []
184
+ }
185
+ ]
186
+ }
@@ -0,0 +1,39 @@
1
+ import { type ReconcilerOwner, type SharedWakeAgentJob } from './coordination';
2
+ import { type WakeAgentQueue } from './WakeAgentQueue';
3
+ export interface ReconcileGroupThreadMessageInput {
4
+ thread: string;
5
+ triggerMessage: string;
6
+ actor?: string;
7
+ role?: 'user' | 'assistant' | 'system' | string;
8
+ content?: string;
9
+ reconcilerOwner?: ReconcilerOwner;
10
+ mentions?: string[];
11
+ routeTargetAgent?: string;
12
+ participants?: string[];
13
+ }
14
+ export interface ReconcileGroupThreadMessageResult {
15
+ wakeJobs: SharedWakeAgentJob[];
16
+ inserted: number;
17
+ skippedReason?: string;
18
+ }
19
+ export interface ServerGroupReconcilerServiceOptions {
20
+ wakeQueue?: WakeAgentQueue;
21
+ redisUrl?: string;
22
+ now?: () => Date;
23
+ }
24
+ /**
25
+ * Protocol-independent group-room Reconciler.
26
+ *
27
+ * It decides only whether a group message should enqueue minimal WakeAgentJob
28
+ * records. It does not choose models, providers, workspaces, or tool placement,
29
+ * and it does not create a durable Reconciler Pod resource.
30
+ */
31
+ export declare class ServerGroupReconcilerService {
32
+ private readonly wakeQueue;
33
+ private readonly now;
34
+ constructor(options?: ServerGroupReconcilerServiceOptions);
35
+ reconcileThreadMessage(input: ReconcileGroupThreadMessageInput): Promise<ReconcileGroupThreadMessageResult>;
36
+ listQueued(thread: string, agent?: string): Promise<SharedWakeAgentJob[]>;
37
+ close(): Promise<void>;
38
+ }
39
+ export declare function normalizeAgentUris(value: unknown): string[];
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ServerGroupReconcilerService = void 0;
4
+ exports.normalizeAgentUris = normalizeAgentUris;
5
+ const coordination_1 = require("./coordination");
6
+ const WakeAgentQueue_1 = require("./WakeAgentQueue");
7
+ /**
8
+ * Protocol-independent group-room Reconciler.
9
+ *
10
+ * It decides only whether a group message should enqueue minimal WakeAgentJob
11
+ * records. It does not choose models, providers, workspaces, or tool placement,
12
+ * and it does not create a durable Reconciler Pod resource.
13
+ */
14
+ class ServerGroupReconcilerService {
15
+ constructor(options = {}) {
16
+ this.wakeQueue = options.wakeQueue ?? (0, WakeAgentQueue_1.createWakeAgentQueue)({ redisUrl: options.redisUrl });
17
+ this.now = options.now ?? (() => new Date());
18
+ }
19
+ async reconcileThreadMessage(input) {
20
+ const reconcilerOwner = (0, coordination_1.normalizeReconcilerOwner)(input.reconcilerOwner, 'server');
21
+ if (reconcilerOwner !== 'server') {
22
+ return { wakeJobs: [], inserted: 0, skippedReason: 'not_server_reconciled' };
23
+ }
24
+ if (input.role && input.role !== 'user') {
25
+ return { wakeJobs: [], inserted: 0, skippedReason: 'not_user_message' };
26
+ }
27
+ const targets = selectWakeTargets({
28
+ mentions: input.mentions,
29
+ routeTargetAgent: input.routeTargetAgent,
30
+ participants: input.participants,
31
+ });
32
+ if (targets.length === 0) {
33
+ return { wakeJobs: [], inserted: 0, skippedReason: 'no_agent_selected' };
34
+ }
35
+ const createdAt = this.now().toISOString();
36
+ const jobs = targets.map(({ agent, reason }) => createWakeJob({
37
+ thread: input.thread,
38
+ triggerMessage: input.triggerMessage,
39
+ agent,
40
+ reason,
41
+ createdAt,
42
+ }));
43
+ let inserted = 0;
44
+ const enqueued = [];
45
+ for (const job of jobs) {
46
+ const result = await this.wakeQueue.enqueue(job);
47
+ if (result.inserted) {
48
+ inserted += 1;
49
+ }
50
+ enqueued.push(result.job);
51
+ }
52
+ return { wakeJobs: enqueued, inserted };
53
+ }
54
+ async listQueued(thread, agent) {
55
+ return this.wakeQueue.listQueued(thread, agent);
56
+ }
57
+ async close() {
58
+ await this.wakeQueue.close?.();
59
+ }
60
+ }
61
+ exports.ServerGroupReconcilerService = ServerGroupReconcilerService;
62
+ function normalizeAgentUris(value) {
63
+ if (typeof value === 'string' && value.length > 0) {
64
+ return [value];
65
+ }
66
+ if (!Array.isArray(value)) {
67
+ return [];
68
+ }
69
+ return Array.from(new Set(value.filter((item) => typeof item === 'string' && item.length > 0)));
70
+ }
71
+ function selectWakeTargets(input) {
72
+ if (input.routeTargetAgent) {
73
+ return [{ agent: input.routeTargetAgent, reason: 'manual' }];
74
+ }
75
+ const participants = new Set(normalizeAgentUris(input.participants));
76
+ return normalizeAgentUris(input.mentions)
77
+ .filter((agent) => participants.size === 0 || participants.has(agent))
78
+ .map((agent) => ({ agent, reason: 'mention' }));
79
+ }
80
+ function createWakeJob(input) {
81
+ return {
82
+ id: (0, WakeAgentQueue_1.sharedWakeAgentJobId)(input),
83
+ thread: input.thread,
84
+ triggerMessage: input.triggerMessage,
85
+ agent: input.agent,
86
+ reason: input.reason,
87
+ status: 'queued',
88
+ createdAt: input.createdAt,
89
+ };
90
+ }
91
+ //# sourceMappingURL=ServerGroupReconcilerService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServerGroupReconcilerService.js","sourceRoot":"","sources":["../../../src/api/reconciler/ServerGroupReconcilerService.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n normalizeReconcilerOwner,\n type ReconcilerOwner,\n type SharedWakeAgentJob,\n type WakeAgentReason,\n} from './coordination';\nimport {\n createWakeAgentQueue,\n sharedWakeAgentJobId,\n type WakeAgentQueue,\n} from './WakeAgentQueue';\n\nexport interface ReconcileGroupThreadMessageInput {\n thread: string;\n triggerMessage: string;\n actor?: string;\n role?: 'user' | 'assistant' | 'system' | string;\n content?: string;\n reconcilerOwner?: ReconcilerOwner;\n mentions?: string[];\n routeTargetAgent?: string;\n participants?: string[];\n}\n\nexport interface ReconcileGroupThreadMessageResult {\n wakeJobs: SharedWakeAgentJob[];\n inserted: number;\n skippedReason?: string;\n}\n\nexport interface ServerGroupReconcilerServiceOptions {\n wakeQueue?: WakeAgentQueue;\n redisUrl?: string;\n now?: () => Date;\n}\n\n/**\n * Protocol-independent group-room Reconciler.\n *\n * It decides only whether a group message should enqueue minimal WakeAgentJob\n * records. It does not choose models, providers, workspaces, or tool placement,\n * and it does not create a durable Reconciler Pod resource.\n */\nexport class ServerGroupReconcilerService {\n private readonly wakeQueue: WakeAgentQueue;\n private readonly now: () => Date;\n\n public constructor(options: ServerGroupReconcilerServiceOptions = {}) {\n this.wakeQueue = options.wakeQueue ?? createWakeAgentQueue({ redisUrl: options.redisUrl });\n this.now = options.now ?? (() => new Date());\n }\n\n public async reconcileThreadMessage(input: ReconcileGroupThreadMessageInput): Promise<ReconcileGroupThreadMessageResult> {\n const reconcilerOwner = normalizeReconcilerOwner(input.reconcilerOwner, 'server');\n if (reconcilerOwner !== 'server') {\n return { wakeJobs: [], inserted: 0, skippedReason: 'not_server_reconciled' };\n }\n if (input.role && input.role !== 'user') {\n return { wakeJobs: [], inserted: 0, skippedReason: 'not_user_message' };\n }\n\n const targets = selectWakeTargets({\n mentions: input.mentions,\n routeTargetAgent: input.routeTargetAgent,\n participants: input.participants,\n });\n if (targets.length === 0) {\n return { wakeJobs: [], inserted: 0, skippedReason: 'no_agent_selected' };\n }\n\n const createdAt = this.now().toISOString();\n const jobs = targets.map(({ agent, reason }) => createWakeJob({\n thread: input.thread,\n triggerMessage: input.triggerMessage,\n agent,\n reason,\n createdAt,\n }));\n\n let inserted = 0;\n const enqueued: SharedWakeAgentJob[] = [];\n for (const job of jobs) {\n const result = await this.wakeQueue.enqueue(job);\n if (result.inserted) {\n inserted += 1;\n }\n enqueued.push(result.job);\n }\n\n return { wakeJobs: enqueued, inserted };\n }\n\n public async listQueued(thread: string, agent?: string): Promise<SharedWakeAgentJob[]> {\n return this.wakeQueue.listQueued(thread, agent);\n }\n\n public async close(): Promise<void> {\n await this.wakeQueue.close?.();\n }\n}\n\nexport function normalizeAgentUris(value: unknown): string[] {\n if (typeof value === 'string' && value.length > 0) {\n return [ value ];\n }\n if (!Array.isArray(value)) {\n return [];\n }\n return Array.from(new Set(value.filter((item): item is string => typeof item === 'string' && item.length > 0)));\n}\n\nfunction selectWakeTargets(input: {\n mentions?: string[];\n routeTargetAgent?: string;\n participants?: string[];\n}): Array<{ agent: string; reason: WakeAgentReason }> {\n if (input.routeTargetAgent) {\n return [{ agent: input.routeTargetAgent, reason: 'manual' }];\n }\n\n const participants = new Set(normalizeAgentUris(input.participants));\n return normalizeAgentUris(input.mentions)\n .filter((agent) => participants.size === 0 || participants.has(agent))\n .map((agent) => ({ agent, reason: 'mention' }));\n}\n\nfunction createWakeJob(input: {\n thread: string;\n triggerMessage: string;\n agent: string;\n reason: WakeAgentReason;\n createdAt: string;\n}): SharedWakeAgentJob {\n return {\n id: sharedWakeAgentJobId(input),\n thread: input.thread,\n triggerMessage: input.triggerMessage,\n agent: input.agent,\n reason: input.reason,\n status: 'queued',\n createdAt: input.createdAt,\n };\n}\n"]}