@openagents-org/agent-launcher 0.2.112 → 0.2.114
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/package.json +1 -1
- package/src/adapters/base.js +10 -15
- package/src/adapters/claude.js +12 -3
- package/src/cli.js +7 -2
- package/src/config.js +169 -1
- package/src/daemon.js +15 -7
- package/src/index.js +39 -5
- package/src/workspace-client.js +23 -0
package/package.json
CHANGED
package/src/adapters/base.js
CHANGED
|
@@ -53,7 +53,7 @@ class BaseAdapter {
|
|
|
53
53
|
this._channelQueues = {};
|
|
54
54
|
this._log = (msg) => {
|
|
55
55
|
const ts = new Date().toISOString();
|
|
56
|
-
console.log(`${ts} INFO adapter: ${msg}`);
|
|
56
|
+
console.log(`${ts} INFO adapter [${this.agentName}]: ${msg}`);
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -109,20 +109,15 @@ class BaseAdapter {
|
|
|
109
109
|
// ------------------------------------------------------------------
|
|
110
110
|
|
|
111
111
|
async _skipExistingEvents() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
if (this._lastEventId) {
|
|
122
|
-
this._log(`Skipped existing events, cursor at ${this._lastEventId}`);
|
|
123
|
-
}
|
|
124
|
-
} catch (e) {
|
|
125
|
-
this._log(`Failed to skip existing events: ${e.message}`);
|
|
112
|
+
// Jump straight to the head with one server call. Pagination from the
|
|
113
|
+
// start was slow and brittle: on a busy workspace it could take many
|
|
114
|
+
// minutes to chew through historical events 200 at a time, leaving the
|
|
115
|
+
// agent silently behind, and a transient mid-paginate empty response
|
|
116
|
+
// (e.g. shared-cache race) would strand the cursor at a non-head id.
|
|
117
|
+
const head = await this.client.getHeadEventId(this.workspaceId, this.token);
|
|
118
|
+
if (head) {
|
|
119
|
+
this._lastEventId = head;
|
|
120
|
+
this._log(`Skipped existing events, cursor at ${head}`);
|
|
126
121
|
}
|
|
127
122
|
}
|
|
128
123
|
|
package/src/adapters/claude.js
CHANGED
|
@@ -390,10 +390,19 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
390
390
|
let mcpConfigFile = null;
|
|
391
391
|
let cmd;
|
|
392
392
|
|
|
393
|
-
// Clean env
|
|
393
|
+
// Clean env: strip every CLAUDE_* / AI_AGENT variable inherited from a
|
|
394
|
+
// parent Claude Code (or Claude Agent SDK) process. If we don't, the
|
|
395
|
+
// spawned `claude` thinks it's running under an SDK harness and picks
|
|
396
|
+
// an org-scoped auth path that returns 403 "Account is no longer a
|
|
397
|
+
// member of the organization" even when the user is logged in fine via
|
|
398
|
+
// `claude login`. We let the child rediscover auth from
|
|
399
|
+
// ~/.claude/.credentials.json (or ANTHROPIC_API_KEY if set).
|
|
394
400
|
const cleanEnv = { ...(this.agentEnv || process.env) };
|
|
395
|
-
|
|
396
|
-
|
|
401
|
+
for (const k of Object.keys(cleanEnv)) {
|
|
402
|
+
if (k.startsWith('CLAUDE_') || k === 'CLAUDECODE' || k === 'AI_AGENT') {
|
|
403
|
+
delete cleanEnv[k];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
397
406
|
|
|
398
407
|
// Run up to 2 attempts: first with session resume, then fresh if stale session detected
|
|
399
408
|
let _shouldRetry = false;
|
package/src/cli.js
CHANGED
|
@@ -117,7 +117,7 @@ async function cmdStatus(connector) {
|
|
|
117
117
|
|
|
118
118
|
async function cmdCreate(connector, flags, positional) {
|
|
119
119
|
const name = positional[0];
|
|
120
|
-
if (!name) { print('Usage: agn create <name> [--type <type>]'); return; }
|
|
120
|
+
if (!name) { print('Usage: agn create <name> [--type <type>] [--install]'); return; }
|
|
121
121
|
const type = flags.type || 'openclaw';
|
|
122
122
|
const role = flags.role || 'worker';
|
|
123
123
|
|
|
@@ -128,8 +128,12 @@ async function cmdCreate(connector, flags, positional) {
|
|
|
128
128
|
// Signal daemon to pick up the new agent
|
|
129
129
|
try { connector.sendDaemonCommand('reload'); } catch {}
|
|
130
130
|
|
|
131
|
-
// Auto-install if not installed
|
|
132
131
|
if (!connector.isInstalled(type)) {
|
|
132
|
+
if (!flags.install) {
|
|
133
|
+
print(`Runtime '${type}' is not installed. Run: agn install ${type}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
133
137
|
print(`Installing ${type}...`);
|
|
134
138
|
try {
|
|
135
139
|
await connector.install(type);
|
|
@@ -513,6 +517,7 @@ Commands:
|
|
|
513
517
|
|
|
514
518
|
Options:
|
|
515
519
|
--config <dir> Config directory (default: ~/.openagents)
|
|
520
|
+
--install Install runtime during create
|
|
516
521
|
`);
|
|
517
522
|
}
|
|
518
523
|
|
package/src/config.js
CHANGED
|
@@ -50,13 +50,14 @@ class Config {
|
|
|
50
50
|
fs.writeFileSync(this.configFile, serializeYaml(config), 'utf-8');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
addAgent({ name, type, role, path: agentPath }) {
|
|
53
|
+
addAgent({ name, type, role, path: agentPath, env }) {
|
|
54
54
|
const config = this.load();
|
|
55
55
|
if (config.agents.some((a) => a.name === name)) {
|
|
56
56
|
throw new Error(`Agent '${name}' already exists`);
|
|
57
57
|
}
|
|
58
58
|
const entry = { name, type: type || 'openclaw', role: role || 'worker' };
|
|
59
59
|
if (agentPath) entry.path = agentPath;
|
|
60
|
+
if (env && Object.keys(env).length > 0) entry.env = env;
|
|
60
61
|
config.agents.push(entry);
|
|
61
62
|
this.save(config);
|
|
62
63
|
return entry;
|
|
@@ -80,6 +81,27 @@ class Config {
|
|
|
80
81
|
return agent;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
updateAgentEnv(name, env) {
|
|
85
|
+
const config = this.load();
|
|
86
|
+
const agent = config.agents.find((a) => a.name === name);
|
|
87
|
+
if (!agent) throw new Error(`Agent '${name}' not found`);
|
|
88
|
+
|
|
89
|
+
const merged = { ...(agent.env || {}), ...(env || {}) };
|
|
90
|
+
const cleaned = {};
|
|
91
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
92
|
+
if (value !== null && value !== undefined && value !== '') cleaned[key] = value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (Object.keys(cleaned).length > 0) {
|
|
96
|
+
agent.env = cleaned;
|
|
97
|
+
} else {
|
|
98
|
+
delete agent.env;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.save(config);
|
|
102
|
+
return agent.env || {};
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
setAgentNetwork(agentName, networkSlug) {
|
|
84
106
|
const config = this.load();
|
|
85
107
|
const agent = config.agents.find((a) => a.name === agentName);
|
|
@@ -191,6 +213,43 @@ class Config {
|
|
|
191
213
|
return { lines: [], size: 0 };
|
|
192
214
|
}
|
|
193
215
|
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Remove log entries whose leading timestamp falls within [start, end].
|
|
219
|
+
* Lines without a parseable timestamp are preserved to avoid deleting
|
|
220
|
+
* stack traces or continuation lines incorrectly.
|
|
221
|
+
* @param {Object} opts - { start, end }
|
|
222
|
+
* @param {string|number|Date} opts.start
|
|
223
|
+
* @param {string|number|Date} opts.end
|
|
224
|
+
* @returns {{ removed: number, remaining: number }}
|
|
225
|
+
*/
|
|
226
|
+
clearLogsInRange(opts = {}) {
|
|
227
|
+
const start = normalizeTimeValue(opts.start);
|
|
228
|
+
const end = normalizeTimeValue(opts.end);
|
|
229
|
+
|
|
230
|
+
if (!start || !end) {
|
|
231
|
+
throw new Error('Start time and end time are required');
|
|
232
|
+
}
|
|
233
|
+
if (start.getTime() > end.getTime()) {
|
|
234
|
+
throw new Error('Start time must be before end time');
|
|
235
|
+
}
|
|
236
|
+
if (!fs.existsSync(this.logFile)) {
|
|
237
|
+
return { removed: 0, remaining: 0 };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const content = fs.readFileSync(this.logFile, 'utf-8');
|
|
241
|
+
const hasTrailingNewline = content.endsWith('\n');
|
|
242
|
+
const allLines = content.split('\n');
|
|
243
|
+
if (hasTrailingNewline) allLines.pop();
|
|
244
|
+
const { keptLines, removed } = filterLogsByTimeRange(allLines, start, end);
|
|
245
|
+
|
|
246
|
+
const nextContent = keptLines.join('\n') + (hasTrailingNewline && keptLines.length > 0 ? '\n' : '');
|
|
247
|
+
const tempFile = `${this.logFile}.tmp`;
|
|
248
|
+
fs.writeFileSync(tempFile, nextContent, 'utf-8');
|
|
249
|
+
fs.renameSync(tempFile, this.logFile);
|
|
250
|
+
|
|
251
|
+
return { removed, remaining: keptLines.length };
|
|
252
|
+
}
|
|
194
253
|
}
|
|
195
254
|
|
|
196
255
|
// -- YAML parser (compatible with Python SDK's daemon.yaml format) --
|
|
@@ -253,6 +312,115 @@ function parseYaml(text) {
|
|
|
253
312
|
return result;
|
|
254
313
|
}
|
|
255
314
|
|
|
315
|
+
function normalizeTimeValue(value) {
|
|
316
|
+
if (value instanceof Date) {
|
|
317
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
318
|
+
}
|
|
319
|
+
if (typeof value === 'number') {
|
|
320
|
+
const date = new Date(value);
|
|
321
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
322
|
+
}
|
|
323
|
+
if (typeof value === 'string' && value.trim()) {
|
|
324
|
+
const date = new Date(value);
|
|
325
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function filterLogsByTimeRange(lines, start, end) {
|
|
331
|
+
const headerTimes = resolveLogHeaderTimestamps(lines, end);
|
|
332
|
+
let activeRemove = false;
|
|
333
|
+
let removed = 0;
|
|
334
|
+
const keptLines = [];
|
|
335
|
+
|
|
336
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
337
|
+
const headerTime = headerTimes[index];
|
|
338
|
+
if (headerTime) {
|
|
339
|
+
const time = headerTime.getTime();
|
|
340
|
+
activeRemove = time >= start.getTime() && time <= end.getTime();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (activeRemove) {
|
|
344
|
+
removed += 1;
|
|
345
|
+
} else {
|
|
346
|
+
keptLines.push(lines[index]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { keptLines, removed };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function resolveLogHeaderTimestamps(lines, referenceTime) {
|
|
354
|
+
const resolved = new Array(lines.length).fill(null);
|
|
355
|
+
let currentDay = startOfLocalDay(referenceTime);
|
|
356
|
+
let lastClockSeconds = null;
|
|
357
|
+
|
|
358
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
359
|
+
const token = parseLogTimestampToken(lines[index]);
|
|
360
|
+
if (!token) continue;
|
|
361
|
+
|
|
362
|
+
if (token.kind === 'iso') {
|
|
363
|
+
resolved[index] = token.date;
|
|
364
|
+
currentDay = startOfLocalDay(token.date);
|
|
365
|
+
lastClockSeconds = (
|
|
366
|
+
token.date.getHours() * 3600 +
|
|
367
|
+
token.date.getMinutes() * 60 +
|
|
368
|
+
token.date.getSeconds()
|
|
369
|
+
);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (lastClockSeconds !== null && token.seconds > lastClockSeconds) {
|
|
374
|
+
currentDay = addLocalDays(currentDay, -1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
resolved[index] = withLocalClock(currentDay, token.seconds);
|
|
378
|
+
lastClockSeconds = token.seconds;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return resolved;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function parseLogTimestampToken(line) {
|
|
385
|
+
if (!line) return null;
|
|
386
|
+
|
|
387
|
+
const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))/);
|
|
388
|
+
if (isoMatch) {
|
|
389
|
+
const date = new Date(isoMatch[1]);
|
|
390
|
+
if (!Number.isNaN(date.getTime())) {
|
|
391
|
+
return { kind: 'iso', date };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const clockMatch = line.match(/^\[(\d{2}):(\d{2}):(\d{2})\]/);
|
|
396
|
+
if (clockMatch) {
|
|
397
|
+
return {
|
|
398
|
+
kind: 'clock',
|
|
399
|
+
seconds:
|
|
400
|
+
Number(clockMatch[1]) * 3600 +
|
|
401
|
+
Number(clockMatch[2]) * 60 +
|
|
402
|
+
Number(clockMatch[3]),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function startOfLocalDay(date) {
|
|
410
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function addLocalDays(date, days) {
|
|
414
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function withLocalClock(day, seconds) {
|
|
418
|
+
const hours = Math.floor(seconds / 3600);
|
|
419
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
420
|
+
const secs = seconds % 60;
|
|
421
|
+
return new Date(day.getFullYear(), day.getMonth(), day.getDate(), hours, minutes, secs);
|
|
422
|
+
}
|
|
423
|
+
|
|
256
424
|
function parseYamlValue(val) {
|
|
257
425
|
if (val === '' || val === 'null' || val === '~') return null;
|
|
258
426
|
if (val === 'true') return true;
|
package/src/daemon.js
CHANGED
|
@@ -70,7 +70,7 @@ class Daemon {
|
|
|
70
70
|
this._writeStatus();
|
|
71
71
|
this._cachedAgentNames = new Set(agents.map(a => a.name));
|
|
72
72
|
this._cachedAgentConfigs = {};
|
|
73
|
-
for (const a of agents) this._cachedAgentConfigs[a.name] = a
|
|
73
|
+
for (const a of agents) this._cachedAgentConfigs[a.name] = this._agentConfigFingerprint(a);
|
|
74
74
|
this._log(`Daemon started with ${agents.length} agent(s)`);
|
|
75
75
|
|
|
76
76
|
// Block until shutdown
|
|
@@ -553,11 +553,19 @@ class Daemon {
|
|
|
553
553
|
_buildAgentEnv(agentCfg) {
|
|
554
554
|
const type = agentCfg.type || 'openclaw';
|
|
555
555
|
const saved = this.envManager.load(type);
|
|
556
|
-
const
|
|
557
|
-
const
|
|
556
|
+
const mergedSaved = { ...saved, ...(agentCfg.env || {}) };
|
|
557
|
+
const resolved = this.envManager.resolve(type, mergedSaved, this.registry);
|
|
558
|
+
const merged = { ...mergedSaved, ...resolved };
|
|
558
559
|
return { ...process.env, ...merged };
|
|
559
560
|
}
|
|
560
561
|
|
|
562
|
+
_agentConfigFingerprint(agentCfg) {
|
|
563
|
+
return JSON.stringify({
|
|
564
|
+
network: agentCfg.network || '',
|
|
565
|
+
env: agentCfg.env || {},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
561
569
|
// ---------------------------------------------------------------------------
|
|
562
570
|
// Internal — agent kill
|
|
563
571
|
// ---------------------------------------------------------------------------
|
|
@@ -682,7 +690,7 @@ class Daemon {
|
|
|
682
690
|
const newAgents = this.config.getAgents();
|
|
683
691
|
const newNames = new Set(newAgents.map(a => a.name));
|
|
684
692
|
const newConfigs = {};
|
|
685
|
-
for (const a of newAgents) newConfigs[a.name] = a
|
|
693
|
+
for (const a of newAgents) newConfigs[a.name] = this._agentConfigFingerprint(a);
|
|
686
694
|
|
|
687
695
|
// Stop removed agents
|
|
688
696
|
for (const name of oldNames) {
|
|
@@ -698,13 +706,13 @@ class Daemon {
|
|
|
698
706
|
await this._ensureAdapterCleared(agent.name);
|
|
699
707
|
this._launchAgent(agent);
|
|
700
708
|
this._log(`Reload: started new agent '${agent.name}'`);
|
|
701
|
-
} else if ((oldConfigs[agent.name] || '') !==
|
|
702
|
-
// Network config changed — restart agent
|
|
709
|
+
} else if ((oldConfigs[agent.name] || '') !== newConfigs[agent.name]) {
|
|
710
|
+
// Network or env config changed — restart agent
|
|
703
711
|
await this.stopAgent(agent.name);
|
|
704
712
|
this._stoppedAgents.delete(agent.name);
|
|
705
713
|
await this._ensureAdapterCleared(agent.name);
|
|
706
714
|
this._launchAgent(agent);
|
|
707
|
-
this._log(`Reload: restarted '${agent.name}' (
|
|
715
|
+
this._log(`Reload: restarted '${agent.name}' (config changed)`);
|
|
708
716
|
}
|
|
709
717
|
}
|
|
710
718
|
|
package/src/index.js
CHANGED
|
@@ -74,22 +74,24 @@ class AgentConnector {
|
|
|
74
74
|
const agents = this.config.getAgents();
|
|
75
75
|
const networks = this.config.getNetworks();
|
|
76
76
|
return agents.map((a) => {
|
|
77
|
-
const
|
|
77
|
+
const type = a.type || 'openclaw';
|
|
78
|
+
const typeEnv = this.env.load(type);
|
|
78
79
|
const network = networks.find((n) => n.slug === a.network || n.id === a.network);
|
|
79
80
|
return {
|
|
80
81
|
name: a.name,
|
|
81
|
-
type
|
|
82
|
+
type,
|
|
82
83
|
role: a.role || 'worker',
|
|
83
84
|
network: a.network || null,
|
|
84
85
|
networkName: network ? (network.name || network.slug) : null,
|
|
85
86
|
path: a.path || null,
|
|
86
|
-
env: { ...
|
|
87
|
+
env: { ...typeEnv, ...(a.env || {}) },
|
|
88
|
+
instanceEnv: { ...(a.env || {}) },
|
|
87
89
|
};
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
addAgent({ name, type, role, path }) {
|
|
92
|
-
this.config.addAgent({ name, type: type || 'openclaw', role: role || 'worker', path });
|
|
93
|
+
addAgent({ name, type, role, path, env }) {
|
|
94
|
+
this.config.addAgent({ name, type: type || 'openclaw', role: role || 'worker', path, env });
|
|
93
95
|
return { success: true };
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -104,6 +106,12 @@ class AgentConnector {
|
|
|
104
106
|
return this.env.load(agentType);
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
getAgentInstanceEnv(agentName) {
|
|
110
|
+
const agent = this.config.getAgent(agentName);
|
|
111
|
+
if (!agent) throw new Error(`Agent '${agentName}' not found`);
|
|
112
|
+
return { ...(agent.env || {}) };
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
saveAgentEnv(agentType, env) {
|
|
108
116
|
this.env.save(agentType, env);
|
|
109
117
|
// Configure native auth for agents that need it (e.g. OpenClaw auth-profiles.json)
|
|
@@ -117,6 +125,24 @@ class AgentConnector {
|
|
|
117
125
|
return { success: true };
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
saveAgentInstanceEnv(agentName, env) {
|
|
129
|
+
const agent = this.config.getAgent(agentName);
|
|
130
|
+
if (!agent) throw new Error(`Agent '${agentName}' not found`);
|
|
131
|
+
const saved = this.config.updateAgentEnv(agentName, env);
|
|
132
|
+
|
|
133
|
+
// Preserve native auth side effects for agents that need them while
|
|
134
|
+
// keeping the model choice scoped to this individual agent.
|
|
135
|
+
try {
|
|
136
|
+
if ((agent.type || 'openclaw') === 'openclaw') {
|
|
137
|
+
const OpenClawAdapter = require('./adapters/openclaw');
|
|
138
|
+
const typeEnv = this.env.load(agent.type || 'openclaw');
|
|
139
|
+
OpenClawAdapter.configureNativeAuth({ ...typeEnv, ...saved });
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
|
|
143
|
+
return { success: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
resolveAgentEnv(agentType, saved) {
|
|
121
147
|
return this.env.resolve(agentType, saved, this.registry);
|
|
122
148
|
}
|
|
@@ -199,6 +225,14 @@ class AgentConnector {
|
|
|
199
225
|
return this.config.getLogs(agentName, lines);
|
|
200
226
|
}
|
|
201
227
|
|
|
228
|
+
tailLogs(opts = {}) {
|
|
229
|
+
return this.config.tailLogs(opts);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
clearLogsInRange(opts = {}) {
|
|
233
|
+
return this.config.clearLogsInRange(opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
202
236
|
// -- Workspace API --
|
|
203
237
|
|
|
204
238
|
async createWorkspace(opts) {
|
package/src/workspace-client.js
CHANGED
|
@@ -179,6 +179,29 @@ class WorkspaceClient {
|
|
|
179
179
|
return events.map((e) => this._eventToMessage(e));
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Fetch the latest workspace.message.posted event id (head cursor).
|
|
184
|
+
* Used by adapters to skip past existing events on join in O(1) instead
|
|
185
|
+
* of paginating from the start. Returns null if the workspace is empty
|
|
186
|
+
* or the request fails.
|
|
187
|
+
*/
|
|
188
|
+
async getHeadEventId(workspaceId, token) {
|
|
189
|
+
try {
|
|
190
|
+
const params = new URLSearchParams({
|
|
191
|
+
network: workspaceId,
|
|
192
|
+
type: 'workspace.message.posted',
|
|
193
|
+
sort: 'desc',
|
|
194
|
+
limit: '1',
|
|
195
|
+
});
|
|
196
|
+
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
197
|
+
const result = data.data || data;
|
|
198
|
+
const events = (result && result.events) || [];
|
|
199
|
+
return events.length > 0 ? (events[0].id || null) : null;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
182
205
|
/**
|
|
183
206
|
* Poll for pending messages targeted at an agent via GET /v1/events.
|
|
184
207
|
* Returns { messages, cursor } where cursor is the last event ID.
|