@undefineds.co/xpod 0.3.48 → 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 (105) 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 +1 -1
  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/store.d.ts +0 -2
  80. package/dist/api/tasks/store.js.map +1 -1
  81. package/dist/cli/commands/auth.d.ts +1 -1
  82. package/dist/cli/commands/auth.js +4 -5
  83. package/dist/cli/commands/auth.js.map +1 -1
  84. package/dist/cli/commands/backup.js +1 -1
  85. package/dist/cli/commands/backup.js.map +1 -1
  86. package/dist/cli/commands/login.js +1 -1
  87. package/dist/cli/commands/login.js.map +1 -1
  88. package/dist/cli/commands/pod.js +1 -1
  89. package/dist/cli/commands/pod.js.map +1 -1
  90. package/dist/cli/lib/auth-helper.d.ts +5 -3
  91. package/dist/cli/lib/auth-helper.js +5 -3
  92. package/dist/cli/lib/auth-helper.js.map +1 -1
  93. package/dist/cli/lib/credentials-store.d.ts +22 -4
  94. package/dist/cli/lib/credentials-store.js +68 -51
  95. package/dist/cli/lib/credentials-store.js.map +1 -1
  96. package/dist/components/components.jsonld +5 -1
  97. package/dist/components/context.jsonld +103 -0
  98. package/dist/index.d.ts +1 -0
  99. package/dist/index.js +15 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/provision/LocalPodProvisioningService.d.ts +1 -0
  102. package/dist/provision/LocalPodProvisioningService.js +9 -0
  103. package/dist/provision/LocalPodProvisioningService.js.map +1 -1
  104. package/dist/provision/LocalPodProvisioningService.jsonld +4 -0
  105. package/package.json +2 -2
@@ -0,0 +1,146 @@
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/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService",
9
+ "@type": "Class",
10
+ "requireElement": "ServerGroupReconcilerService",
11
+ "comment": "Protocol-independent group-room Reconciler. It decides only whether a group message should enqueue minimal WakeAgentJob records. It does not choose models, providers, workspaces, or tool placement, and it does not create a durable Reconciler Pod resource.",
12
+ "parameters": [
13
+ {
14
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService_options",
15
+ "range": {
16
+ "@type": "ParameterRangeUnion",
17
+ "parameterRangeElements": [
18
+ "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerServiceOptions",
19
+ {
20
+ "@type": "ParameterRangeUndefined"
21
+ }
22
+ ]
23
+ }
24
+ }
25
+ ],
26
+ "memberFields": [
27
+ {
28
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService__member_wakeQueue",
29
+ "memberFieldName": "wakeQueue"
30
+ },
31
+ {
32
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService__member_now",
33
+ "memberFieldName": "now"
34
+ },
35
+ {
36
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService__member_constructor",
37
+ "memberFieldName": "constructor"
38
+ },
39
+ {
40
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService__member_reconcileThreadMessage",
41
+ "memberFieldName": "reconcileThreadMessage"
42
+ },
43
+ {
44
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService__member_listQueued",
45
+ "memberFieldName": "listQueued"
46
+ },
47
+ {
48
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService__member_close",
49
+ "memberFieldName": "close"
50
+ }
51
+ ],
52
+ "constructorArguments": [
53
+ {
54
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerService_options"
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput",
60
+ "@type": "AbstractClass",
61
+ "requireElement": "ReconcileGroupThreadMessageInput",
62
+ "parameters": [],
63
+ "memberFields": [
64
+ {
65
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_thread",
66
+ "memberFieldName": "thread"
67
+ },
68
+ {
69
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_triggerMessage",
70
+ "memberFieldName": "triggerMessage"
71
+ },
72
+ {
73
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_actor",
74
+ "memberFieldName": "actor"
75
+ },
76
+ {
77
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_role",
78
+ "memberFieldName": "role"
79
+ },
80
+ {
81
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_content",
82
+ "memberFieldName": "content"
83
+ },
84
+ {
85
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_reconcilerOwner",
86
+ "memberFieldName": "reconcilerOwner"
87
+ },
88
+ {
89
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_mentions",
90
+ "memberFieldName": "mentions"
91
+ },
92
+ {
93
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_routeTargetAgent",
94
+ "memberFieldName": "routeTargetAgent"
95
+ },
96
+ {
97
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageInput__member_participants",
98
+ "memberFieldName": "participants"
99
+ }
100
+ ],
101
+ "constructorArguments": []
102
+ },
103
+ {
104
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageResult",
105
+ "@type": "AbstractClass",
106
+ "requireElement": "ReconcileGroupThreadMessageResult",
107
+ "parameters": [],
108
+ "memberFields": [
109
+ {
110
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageResult__member_wakeJobs",
111
+ "memberFieldName": "wakeJobs"
112
+ },
113
+ {
114
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageResult__member_inserted",
115
+ "memberFieldName": "inserted"
116
+ },
117
+ {
118
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ReconcileGroupThreadMessageResult__member_skippedReason",
119
+ "memberFieldName": "skippedReason"
120
+ }
121
+ ],
122
+ "constructorArguments": []
123
+ },
124
+ {
125
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerServiceOptions",
126
+ "@type": "AbstractClass",
127
+ "requireElement": "ServerGroupReconcilerServiceOptions",
128
+ "parameters": [],
129
+ "memberFields": [
130
+ {
131
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerServiceOptions__member_wakeQueue",
132
+ "memberFieldName": "wakeQueue"
133
+ },
134
+ {
135
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerServiceOptions__member_redisUrl",
136
+ "memberFieldName": "redisUrl"
137
+ },
138
+ {
139
+ "@id": "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld#ServerGroupReconcilerServiceOptions__member_now",
140
+ "memberFieldName": "now"
141
+ }
142
+ ],
143
+ "constructorArguments": []
144
+ }
145
+ ]
146
+ }
@@ -0,0 +1,23 @@
1
+ import { type SharedWakeAgentJob } from './coordination';
2
+ export interface WakeAgentEnqueueResult {
3
+ job: SharedWakeAgentJob;
4
+ inserted: boolean;
5
+ }
6
+ export interface WakeAgentQueue {
7
+ enqueue(job: SharedWakeAgentJob): Promise<WakeAgentEnqueueResult>;
8
+ listQueued(thread: string, agent?: string): Promise<SharedWakeAgentJob[]>;
9
+ close?(): Promise<void>;
10
+ }
11
+ export interface WakeAgentQueueOptions {
12
+ redisUrl?: string;
13
+ namespace?: string;
14
+ }
15
+ export declare function createWakeAgentQueue(options?: WakeAgentQueueOptions): WakeAgentQueue;
16
+ export declare function sharedWakeAgentJobId(input: Pick<SharedWakeAgentJob, 'thread' | 'triggerMessage' | 'agent'>): string;
17
+ export declare function wakeAgentQueueKey(job: Pick<SharedWakeAgentJob, 'thread' | 'agent'>): string;
18
+ export declare class InMemoryWakeAgentQueue implements WakeAgentQueue {
19
+ private readonly jobsByDedupeKey;
20
+ private readonly queueKeysByDedupeKey;
21
+ enqueue(job: SharedWakeAgentJob): Promise<WakeAgentEnqueueResult>;
22
+ listQueued(thread: string, agent?: string): Promise<SharedWakeAgentJob[]>;
23
+ }
@@ -0,0 +1,123 @@
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.InMemoryWakeAgentQueue = void 0;
7
+ exports.createWakeAgentQueue = createWakeAgentQueue;
8
+ exports.sharedWakeAgentJobId = sharedWakeAgentJobId;
9
+ exports.wakeAgentQueueKey = wakeAgentQueueKey;
10
+ const node_crypto_1 = require("node:crypto");
11
+ const ioredis_1 = __importDefault(require("ioredis"));
12
+ const global_logger_factory_1 = require("global-logger-factory");
13
+ const coordination_1 = require("./coordination");
14
+ const RedisClientLifecycle_1 = require("../../storage/redis/RedisClientLifecycle");
15
+ function createWakeAgentQueue(options = {}) {
16
+ return options.redisUrl
17
+ ? new RedisWakeAgentQueue(options)
18
+ : new InMemoryWakeAgentQueue();
19
+ }
20
+ function sharedWakeAgentJobId(input) {
21
+ return `wake_${hash((0, coordination_1.sharedWakeAgentJobDedupeKey)(input))}`;
22
+ }
23
+ function wakeAgentQueueKey(job) {
24
+ return `steer_queue:${job.thread}:${job.agent}`;
25
+ }
26
+ class InMemoryWakeAgentQueue {
27
+ constructor() {
28
+ this.jobsByDedupeKey = new Map();
29
+ this.queueKeysByDedupeKey = new Map();
30
+ }
31
+ async enqueue(job) {
32
+ const dedupeKey = (0, coordination_1.sharedWakeAgentJobDedupeKey)(job);
33
+ const existing = this.jobsByDedupeKey.get(dedupeKey);
34
+ if (existing) {
35
+ return { job: { ...existing }, inserted: false };
36
+ }
37
+ const stored = { ...job };
38
+ this.jobsByDedupeKey.set(dedupeKey, stored);
39
+ this.queueKeysByDedupeKey.set(dedupeKey, wakeAgentQueueKey(stored));
40
+ return { job: { ...stored }, inserted: true };
41
+ }
42
+ async listQueued(thread, agent) {
43
+ return Array.from(this.jobsByDedupeKey.entries())
44
+ .filter(([dedupeKey, job]) => (job.thread === thread
45
+ && (!agent || job.agent === agent)
46
+ && this.queueKeysByDedupeKey.has(dedupeKey)))
47
+ .map(([, job]) => ({ ...job }));
48
+ }
49
+ }
50
+ exports.InMemoryWakeAgentQueue = InMemoryWakeAgentQueue;
51
+ class RedisWakeAgentQueue {
52
+ constructor(options) {
53
+ this.logger = (0, global_logger_factory_1.getLoggerFor)(this);
54
+ this.shuttingDown = false;
55
+ if (!options.redisUrl) {
56
+ throw new Error('redisUrl is required for RedisWakeAgentQueue');
57
+ }
58
+ this.namespace = options.namespace ?? 'xpod:wake:';
59
+ this.redis = new ioredis_1.default(options.redisUrl, { lazyConnect: false });
60
+ (0, RedisClientLifecycle_1.attachRedisClientErrorHandler)(this.redis, {
61
+ logger: this.logger,
62
+ label: 'WakeAgentQueue',
63
+ isShuttingDown: () => this.shuttingDown,
64
+ });
65
+ }
66
+ async enqueue(job) {
67
+ const dedupeKey = this.dedupeStorageKey(job);
68
+ const payload = JSON.stringify(job);
69
+ const inserted = await this.redis.set(dedupeKey, payload, 'NX');
70
+ if (inserted === 'OK') {
71
+ await this.redis.rpush(this.queueStorageKey(job.thread, job.agent), payload);
72
+ await this.redis.sadd(this.threadAgentsKey(job.thread), job.agent);
73
+ return { job: { ...job }, inserted: true };
74
+ }
75
+ const existing = parseJson(await this.redis.get(dedupeKey)) ?? job;
76
+ return { job: existing, inserted: false };
77
+ }
78
+ async listQueued(thread, agent) {
79
+ const agents = agent ? [agent] : await this.redis.smembers(this.threadAgentsKey(thread));
80
+ const jobs = [];
81
+ for (const agent of agents) {
82
+ const raws = await this.redis.lrange(this.queueStorageKey(thread, agent), 0, -1);
83
+ for (const raw of raws) {
84
+ const job = parseJson(raw);
85
+ if (job) {
86
+ jobs.push(job);
87
+ }
88
+ }
89
+ }
90
+ return jobs;
91
+ }
92
+ async close() {
93
+ this.shuttingDown = true;
94
+ await (0, RedisClientLifecycle_1.closeRedisClient)(this.redis, {
95
+ logger: this.logger,
96
+ label: 'WakeAgentQueue',
97
+ });
98
+ }
99
+ dedupeStorageKey(job) {
100
+ return `${this.namespace}dedupe:${hash((0, coordination_1.sharedWakeAgentJobDedupeKey)(job))}`;
101
+ }
102
+ queueStorageKey(thread, agent) {
103
+ return `${this.namespace}${wakeAgentQueueKey({ thread, agent })}`;
104
+ }
105
+ threadAgentsKey(thread) {
106
+ return `${this.namespace}thread_agents:${hash(thread)}`;
107
+ }
108
+ }
109
+ function hash(value) {
110
+ return (0, node_crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, 32);
111
+ }
112
+ function parseJson(raw) {
113
+ if (!raw) {
114
+ return undefined;
115
+ }
116
+ try {
117
+ return JSON.parse(raw);
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ }
123
+ //# sourceMappingURL=WakeAgentQueue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WakeAgentQueue.js","sourceRoot":"","sources":["../../../src/api/reconciler/WakeAgentQueue.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport Redis from 'ioredis';\nimport { getLoggerFor } from 'global-logger-factory';\nimport {\n sharedWakeAgentJobDedupeKey,\n type SharedWakeAgentJob,\n} from './coordination';\nimport {\n attachRedisClientErrorHandler,\n closeRedisClient,\n} from '../../storage/redis/RedisClientLifecycle';\n\nexport interface WakeAgentEnqueueResult {\n job: SharedWakeAgentJob;\n inserted: boolean;\n}\n\nexport interface WakeAgentQueue {\n enqueue(job: SharedWakeAgentJob): Promise<WakeAgentEnqueueResult>;\n listQueued(thread: string, agent?: string): Promise<SharedWakeAgentJob[]>;\n close?(): Promise<void>;\n}\n\nexport interface WakeAgentQueueOptions {\n redisUrl?: string;\n namespace?: string;\n}\n\nexport function createWakeAgentQueue(options: WakeAgentQueueOptions = {}): WakeAgentQueue {\n return options.redisUrl\n ? new RedisWakeAgentQueue(options)\n : new InMemoryWakeAgentQueue();\n}\n\nexport function sharedWakeAgentJobId(input: Pick<SharedWakeAgentJob, 'thread' | 'triggerMessage' | 'agent'>): string {\n return `wake_${hash(sharedWakeAgentJobDedupeKey(input))}`;\n}\n\nexport function wakeAgentQueueKey(job: Pick<SharedWakeAgentJob, 'thread' | 'agent'>): string {\n return `steer_queue:${job.thread}:${job.agent}`;\n}\n\nexport class InMemoryWakeAgentQueue implements WakeAgentQueue {\n private readonly jobsByDedupeKey = new Map<string, SharedWakeAgentJob>();\n private readonly queueKeysByDedupeKey = new Map<string, string>();\n\n public async enqueue(job: SharedWakeAgentJob): Promise<WakeAgentEnqueueResult> {\n const dedupeKey = sharedWakeAgentJobDedupeKey(job);\n const existing = this.jobsByDedupeKey.get(dedupeKey);\n if (existing) {\n return { job: { ...existing }, inserted: false };\n }\n const stored = { ...job };\n this.jobsByDedupeKey.set(dedupeKey, stored);\n this.queueKeysByDedupeKey.set(dedupeKey, wakeAgentQueueKey(stored));\n return { job: { ...stored }, inserted: true };\n }\n\n public async listQueued(thread: string, agent?: string): Promise<SharedWakeAgentJob[]> {\n return Array.from(this.jobsByDedupeKey.entries())\n .filter(([ dedupeKey, job ]) => (\n job.thread === thread\n && (!agent || job.agent === agent)\n && this.queueKeysByDedupeKey.has(dedupeKey)\n ))\n .map(([, job]) => ({ ...job }));\n }\n}\n\nclass RedisWakeAgentQueue implements WakeAgentQueue {\n private readonly logger = getLoggerFor(this);\n private readonly redis: Redis;\n private readonly namespace: string;\n private shuttingDown = false;\n\n public constructor(options: WakeAgentQueueOptions) {\n if (!options.redisUrl) {\n throw new Error('redisUrl is required for RedisWakeAgentQueue');\n }\n this.namespace = options.namespace ?? 'xpod:wake:';\n this.redis = new Redis(options.redisUrl, { lazyConnect: false });\n attachRedisClientErrorHandler(this.redis, {\n logger: this.logger,\n label: 'WakeAgentQueue',\n isShuttingDown: () => this.shuttingDown,\n });\n }\n\n public async enqueue(job: SharedWakeAgentJob): Promise<WakeAgentEnqueueResult> {\n const dedupeKey = this.dedupeStorageKey(job);\n const payload = JSON.stringify(job);\n const inserted = await this.redis.set(dedupeKey, payload, 'NX');\n if (inserted === 'OK') {\n await this.redis.rpush(this.queueStorageKey(job.thread, job.agent), payload);\n await this.redis.sadd(this.threadAgentsKey(job.thread), job.agent);\n return { job: { ...job }, inserted: true };\n }\n\n const existing = parseJson<SharedWakeAgentJob>(await this.redis.get(dedupeKey)) ?? job;\n return { job: existing, inserted: false };\n }\n\n public async listQueued(thread: string, agent?: string): Promise<SharedWakeAgentJob[]> {\n const agents = agent ? [ agent ] : await this.redis.smembers(this.threadAgentsKey(thread));\n const jobs: SharedWakeAgentJob[] = [];\n for (const agent of agents) {\n const raws = await this.redis.lrange(this.queueStorageKey(thread, agent), 0, -1);\n for (const raw of raws) {\n const job = parseJson<SharedWakeAgentJob>(raw);\n if (job) {\n jobs.push(job);\n }\n }\n }\n return jobs;\n }\n\n public async close(): Promise<void> {\n this.shuttingDown = true;\n await closeRedisClient(this.redis, {\n logger: this.logger,\n label: 'WakeAgentQueue',\n });\n }\n\n private dedupeStorageKey(job: Pick<SharedWakeAgentJob, 'thread' | 'triggerMessage' | 'agent'>): string {\n return `${this.namespace}dedupe:${hash(sharedWakeAgentJobDedupeKey(job))}`;\n }\n\n private queueStorageKey(thread: string, agent: string): string {\n return `${this.namespace}${wakeAgentQueueKey({ thread, agent })}`;\n }\n\n private threadAgentsKey(thread: string): string {\n return `${this.namespace}thread_agents:${hash(thread)}`;\n }\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,91 @@
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/WakeAgentQueue.jsonld#InMemoryWakeAgentQueue",
9
+ "@type": "Class",
10
+ "requireElement": "InMemoryWakeAgentQueue",
11
+ "extends": [
12
+ "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueue"
13
+ ],
14
+ "parameters": [],
15
+ "memberFields": [
16
+ {
17
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#InMemoryWakeAgentQueue__member_jobsByDedupeKey",
18
+ "memberFieldName": "jobsByDedupeKey"
19
+ },
20
+ {
21
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#InMemoryWakeAgentQueue__member_queueKeysByDedupeKey",
22
+ "memberFieldName": "queueKeysByDedupeKey"
23
+ },
24
+ {
25
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#InMemoryWakeAgentQueue__member_enqueue",
26
+ "memberFieldName": "enqueue"
27
+ },
28
+ {
29
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#InMemoryWakeAgentQueue__member_listQueued",
30
+ "memberFieldName": "listQueued"
31
+ }
32
+ ],
33
+ "constructorArguments": []
34
+ },
35
+ {
36
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentEnqueueResult",
37
+ "@type": "AbstractClass",
38
+ "requireElement": "WakeAgentEnqueueResult",
39
+ "parameters": [],
40
+ "memberFields": [
41
+ {
42
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentEnqueueResult__member_job",
43
+ "memberFieldName": "job"
44
+ },
45
+ {
46
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentEnqueueResult__member_inserted",
47
+ "memberFieldName": "inserted"
48
+ }
49
+ ],
50
+ "constructorArguments": []
51
+ },
52
+ {
53
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueue",
54
+ "@type": "AbstractClass",
55
+ "requireElement": "WakeAgentQueue",
56
+ "parameters": [],
57
+ "memberFields": [
58
+ {
59
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueue__member_enqueue",
60
+ "memberFieldName": "enqueue"
61
+ },
62
+ {
63
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueue__member_listQueued",
64
+ "memberFieldName": "listQueued"
65
+ },
66
+ {
67
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueue__member_close",
68
+ "memberFieldName": "close"
69
+ }
70
+ ],
71
+ "constructorArguments": []
72
+ },
73
+ {
74
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueueOptions",
75
+ "@type": "AbstractClass",
76
+ "requireElement": "WakeAgentQueueOptions",
77
+ "parameters": [],
78
+ "memberFields": [
79
+ {
80
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueueOptions__member_redisUrl",
81
+ "memberFieldName": "redisUrl"
82
+ },
83
+ {
84
+ "@id": "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld#WakeAgentQueueOptions__member_namespace",
85
+ "memberFieldName": "namespace"
86
+ }
87
+ ],
88
+ "constructorArguments": []
89
+ }
90
+ ]
91
+ }
@@ -0,0 +1,61 @@
1
+ export type ReconcilerOwner = 'client' | 'server';
2
+ export type WakeAgentReason = 'mention' | 'reconciler_decision' | 'manual';
3
+ export type WakeAgentStatus = 'queued' | 'leased' | 'completed' | 'failed';
4
+ export type ClientKind = 'cli' | 'desktop' | 'mobile' | 'web';
5
+ export interface ReconcilerCoordinationMetadata {
6
+ reconcilerOwner: ReconcilerOwner;
7
+ }
8
+ export interface SharedWakeAgentJob {
9
+ id: string;
10
+ thread: string;
11
+ triggerMessage: string;
12
+ agent: string;
13
+ reason: WakeAgentReason;
14
+ status: WakeAgentStatus;
15
+ createdAt: string;
16
+ }
17
+ export interface WakeAgentLeaseFields {
18
+ priority?: 'low' | 'normal' | 'high';
19
+ leaseOwner?: string;
20
+ leaseExpiresAt?: string;
21
+ }
22
+ export interface ClientCapability {
23
+ clientId: string;
24
+ kind: ClientKind;
25
+ user: string;
26
+ canCoordinateClientOwnedThread: boolean;
27
+ canRunAgent: boolean;
28
+ workspaces: string[];
29
+ heartbeatAt: string;
30
+ }
31
+ export interface ClientReconcilerLease {
32
+ thread: string;
33
+ ownerClientId: string;
34
+ ownerUser: string;
35
+ fencingToken: string;
36
+ expiresAt: string;
37
+ }
38
+ export interface ClientReconcilerActivationOptions {
39
+ thread: string;
40
+ ownerUser: string;
41
+ clients: ClientCapability[];
42
+ currentLease?: ClientReconcilerLease;
43
+ now?: Date;
44
+ heartbeatTtlMs?: number;
45
+ leaseTtlMs?: number;
46
+ fencingToken?: string;
47
+ }
48
+ export declare function activateClientReconciler(options: ClientReconcilerActivationOptions): ClientReconcilerLease | undefined;
49
+ export declare function isReconcilerOwner(value: unknown): value is ReconcilerOwner;
50
+ export declare function normalizeReconcilerOwner(value: unknown, fallback?: ReconcilerOwner): ReconcilerOwner;
51
+ export declare function reconcilerCoordinationMetadata(owner: ReconcilerOwner): ReconcilerCoordinationMetadata;
52
+ export declare function withReconcilerCoordinationMetadata(metadata: Record<string, unknown> | undefined, owner: ReconcilerOwner): Record<string, unknown>;
53
+ export declare function sharedWakeAgentJobDedupeKey(job: Pick<SharedWakeAgentJob, 'thread' | 'triggerMessage' | 'agent'>): string;
54
+ export declare function isClientReconcilerLeaseActive(lease: ClientReconcilerLease | undefined, now?: Date): boolean;
55
+ export declare function clientKindRank(kind: ClientKind): number;
56
+ export declare function selectClientReconcilerClient(clients: ClientCapability[], options: {
57
+ ownerUser: string;
58
+ now?: Date;
59
+ heartbeatTtlMs?: number;
60
+ currentLease?: ClientReconcilerLease;
61
+ }): ClientCapability | undefined;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.activateClientReconciler = activateClientReconciler;
4
+ exports.isReconcilerOwner = isReconcilerOwner;
5
+ exports.normalizeReconcilerOwner = normalizeReconcilerOwner;
6
+ exports.reconcilerCoordinationMetadata = reconcilerCoordinationMetadata;
7
+ exports.withReconcilerCoordinationMetadata = withReconcilerCoordinationMetadata;
8
+ exports.sharedWakeAgentJobDedupeKey = sharedWakeAgentJobDedupeKey;
9
+ exports.isClientReconcilerLeaseActive = isClientReconcilerLeaseActive;
10
+ exports.clientKindRank = clientKindRank;
11
+ exports.selectClientReconcilerClient = selectClientReconcilerClient;
12
+ function activateClientReconciler(options) {
13
+ const now = options.now ?? new Date();
14
+ const leaseTtlMs = options.leaseTtlMs ?? 30_000;
15
+ const selected = selectClientReconcilerClient(options.clients, {
16
+ ownerUser: options.ownerUser,
17
+ now,
18
+ heartbeatTtlMs: options.heartbeatTtlMs,
19
+ currentLease: options.currentLease,
20
+ });
21
+ if (!selected) {
22
+ return undefined;
23
+ }
24
+ const keepCurrentToken = options.currentLease?.ownerClientId === selected.clientId
25
+ && options.currentLease.ownerUser === options.ownerUser
26
+ && isClientReconcilerLeaseActive(options.currentLease, now);
27
+ return {
28
+ thread: options.thread,
29
+ ownerClientId: selected.clientId,
30
+ ownerUser: options.ownerUser,
31
+ fencingToken: keepCurrentToken
32
+ ? options.currentLease.fencingToken
33
+ : (options.fencingToken ?? `${selected.clientId}:${now.toISOString()}`),
34
+ expiresAt: new Date(now.getTime() + leaseTtlMs).toISOString(),
35
+ };
36
+ }
37
+ function isReconcilerOwner(value) {
38
+ return value === 'client' || value === 'server';
39
+ }
40
+ function normalizeReconcilerOwner(value, fallback = 'client') {
41
+ return isReconcilerOwner(value) ? value : fallback;
42
+ }
43
+ function reconcilerCoordinationMetadata(owner) {
44
+ return { reconcilerOwner: owner };
45
+ }
46
+ function withReconcilerCoordinationMetadata(metadata, owner) {
47
+ const { conversationKind: _discardedConversationKind, ...rest } = metadata ?? {};
48
+ void _discardedConversationKind;
49
+ return {
50
+ ...rest,
51
+ ...reconcilerCoordinationMetadata(owner),
52
+ };
53
+ }
54
+ function sharedWakeAgentJobDedupeKey(job) {
55
+ return [job.thread, job.triggerMessage, job.agent].join('|');
56
+ }
57
+ function isClientReconcilerLeaseActive(lease, now = new Date()) {
58
+ if (!lease) {
59
+ return false;
60
+ }
61
+ const expiresAt = Date.parse(lease.expiresAt);
62
+ return Number.isFinite(expiresAt) && expiresAt > now.getTime();
63
+ }
64
+ function clientKindRank(kind) {
65
+ switch (kind) {
66
+ case 'cli':
67
+ case 'desktop':
68
+ return 0;
69
+ case 'mobile':
70
+ return 1;
71
+ case 'web':
72
+ return 2;
73
+ default:
74
+ return 3;
75
+ }
76
+ }
77
+ function selectClientReconcilerClient(clients, options) {
78
+ const now = options.now ?? new Date();
79
+ const heartbeatTtlMs = options.heartbeatTtlMs ?? 30_000;
80
+ const activeLease = isClientReconcilerLeaseActive(options.currentLease, now)
81
+ ? options.currentLease
82
+ : undefined;
83
+ if (activeLease) {
84
+ const leaseOwner = clients.find((client) => (client.clientId === activeLease.ownerClientId
85
+ && client.user === options.ownerUser
86
+ && client.canCoordinateClientOwnedThread
87
+ && isClientHeartbeatFresh(client, now, heartbeatTtlMs)));
88
+ if (leaseOwner) {
89
+ return leaseOwner;
90
+ }
91
+ }
92
+ return clients
93
+ .filter((client) => (client.user === options.ownerUser
94
+ && client.canCoordinateClientOwnedThread
95
+ && isClientHeartbeatFresh(client, now, heartbeatTtlMs)))
96
+ .sort(compareClientCapability)[0];
97
+ }
98
+ function isClientHeartbeatFresh(client, now, heartbeatTtlMs) {
99
+ const heartbeatAt = Date.parse(client.heartbeatAt);
100
+ return Number.isFinite(heartbeatAt) && now.getTime() - heartbeatAt <= heartbeatTtlMs;
101
+ }
102
+ function compareClientCapability(a, b) {
103
+ const rank = clientKindRank(a.kind) - clientKindRank(b.kind);
104
+ if (rank !== 0) {
105
+ return rank;
106
+ }
107
+ return a.clientId.localeCompare(b.clientId);
108
+ }
109
+ //# sourceMappingURL=coordination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coordination.js","sourceRoot":"","sources":["../../../src/api/reconciler/coordination.ts"],"names":[],"mappings":"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","sourcesContent":["export type ReconcilerOwner = 'client' | 'server';\nexport type WakeAgentReason = 'mention' | 'reconciler_decision' | 'manual';\nexport type WakeAgentStatus = 'queued' | 'leased' | 'completed' | 'failed';\nexport type ClientKind = 'cli' | 'desktop' | 'mobile' | 'web';\n\nexport interface ReconcilerCoordinationMetadata {\n reconcilerOwner: ReconcilerOwner;\n}\n\nexport interface SharedWakeAgentJob {\n id: string;\n thread: string;\n triggerMessage: string;\n agent: string;\n reason: WakeAgentReason;\n status: WakeAgentStatus;\n createdAt: string;\n}\n\nexport interface WakeAgentLeaseFields {\n priority?: 'low' | 'normal' | 'high';\n leaseOwner?: string;\n leaseExpiresAt?: string;\n}\n\nexport interface ClientCapability {\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 ClientReconcilerLease {\n thread: string;\n ownerClientId: string;\n ownerUser: string;\n fencingToken: string;\n expiresAt: string;\n}\n\nexport interface ClientReconcilerActivationOptions {\n thread: string;\n ownerUser: string;\n clients: ClientCapability[];\n currentLease?: ClientReconcilerLease;\n now?: Date;\n heartbeatTtlMs?: number;\n leaseTtlMs?: number;\n fencingToken?: string;\n}\n\nexport function activateClientReconciler(options: ClientReconcilerActivationOptions): ClientReconcilerLease | undefined {\n const now = options.now ?? new Date();\n const leaseTtlMs = options.leaseTtlMs ?? 30_000;\n const selected = selectClientReconcilerClient(options.clients, {\n ownerUser: options.ownerUser,\n now,\n heartbeatTtlMs: options.heartbeatTtlMs,\n currentLease: options.currentLease,\n });\n\n if (!selected) {\n return undefined;\n }\n\n const keepCurrentToken = options.currentLease?.ownerClientId === selected.clientId\n && options.currentLease.ownerUser === options.ownerUser\n && isClientReconcilerLeaseActive(options.currentLease, now);\n\n return {\n thread: options.thread,\n ownerClientId: selected.clientId,\n ownerUser: options.ownerUser,\n fencingToken: keepCurrentToken\n ? options.currentLease!.fencingToken\n : (options.fencingToken ?? `${selected.clientId}:${now.toISOString()}`),\n expiresAt: new Date(now.getTime() + leaseTtlMs).toISOString(),\n };\n}\n\nexport function isReconcilerOwner(value: unknown): value is ReconcilerOwner {\n return value === 'client' || value === 'server';\n}\n\nexport function normalizeReconcilerOwner(value: unknown, fallback: ReconcilerOwner = 'client'): ReconcilerOwner {\n return isReconcilerOwner(value) ? value : fallback;\n}\n\nexport function reconcilerCoordinationMetadata(owner: ReconcilerOwner): ReconcilerCoordinationMetadata {\n return { reconcilerOwner: owner };\n}\n\nexport function withReconcilerCoordinationMetadata(\n metadata: Record<string, unknown> | undefined,\n owner: ReconcilerOwner,\n): Record<string, unknown> {\n const { conversationKind: _discardedConversationKind, ...rest } = metadata ?? {};\n void _discardedConversationKind;\n return {\n ...rest,\n ...reconcilerCoordinationMetadata(owner),\n };\n}\n\nexport function sharedWakeAgentJobDedupeKey(job: Pick<SharedWakeAgentJob, 'thread' | 'triggerMessage' | 'agent'>): string {\n return [job.thread, job.triggerMessage, job.agent].join('|');\n}\n\nexport function isClientReconcilerLeaseActive(lease: ClientReconcilerLease | undefined, now: Date = new Date()): boolean {\n if (!lease) {\n return false;\n }\n const expiresAt = Date.parse(lease.expiresAt);\n return Number.isFinite(expiresAt) && expiresAt > now.getTime();\n}\n\nexport function clientKindRank(kind: ClientKind): number {\n switch (kind) {\n case 'cli':\n case 'desktop':\n return 0;\n case 'mobile':\n return 1;\n case 'web':\n return 2;\n default:\n return 3;\n }\n}\n\nexport function selectClientReconcilerClient(\n clients: ClientCapability[],\n options: {\n ownerUser: string;\n now?: Date;\n heartbeatTtlMs?: number;\n currentLease?: ClientReconcilerLease;\n },\n): ClientCapability | undefined {\n const now = options.now ?? new Date();\n const heartbeatTtlMs = options.heartbeatTtlMs ?? 30_000;\n const activeLease = isClientReconcilerLeaseActive(options.currentLease, now)\n ? options.currentLease\n : undefined;\n\n if (activeLease) {\n const leaseOwner = clients.find((client) => (\n client.clientId === activeLease.ownerClientId\n && client.user === options.ownerUser\n && client.canCoordinateClientOwnedThread\n && isClientHeartbeatFresh(client, now, heartbeatTtlMs)\n ));\n if (leaseOwner) {\n return leaseOwner;\n }\n }\n\n return clients\n .filter((client) => (\n client.user === options.ownerUser\n && client.canCoordinateClientOwnedThread\n && isClientHeartbeatFresh(client, now, heartbeatTtlMs)\n ))\n .sort(compareClientCapability)[0];\n}\n\nfunction isClientHeartbeatFresh(client: ClientCapability, now: Date, heartbeatTtlMs: number): boolean {\n const heartbeatAt = Date.parse(client.heartbeatAt);\n return Number.isFinite(heartbeatAt) && now.getTime() - heartbeatAt <= heartbeatTtlMs;\n}\n\nfunction compareClientCapability(a: ClientCapability, b: ClientCapability): number {\n const rank = clientKindRank(a.kind) - clientKindRank(b.kind);\n if (rank !== 0) {\n return rank;\n }\n return a.clientId.localeCompare(b.clientId);\n}\n"]}