rogerrat 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +38 -2
- package/dist/channel.js +23 -27
- package/dist/landing.js +14 -2
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -363,6 +363,41 @@ export function createApp(opts) {
|
|
|
363
363
|
return c.json({ error: "webhook not found or not yours" }, 404);
|
|
364
364
|
return c.json({ ok: true });
|
|
365
365
|
});
|
|
366
|
+
app.get("/api/channels/:id", (c) => {
|
|
367
|
+
const channelId = c.req.param("id");
|
|
368
|
+
if (!channelExists(channelId)) {
|
|
369
|
+
return c.json({
|
|
370
|
+
error: "channel not found",
|
|
371
|
+
hint: `Create one with: POST ${opts.publicOrigin}/api/channels (no auth required). See ${opts.publicOrigin}/llms.txt for the quickstart.`,
|
|
372
|
+
}, 404);
|
|
373
|
+
}
|
|
374
|
+
const base = `${opts.publicOrigin}/api/channels/${channelId}`;
|
|
375
|
+
return c.json({
|
|
376
|
+
channel_id: channelId,
|
|
377
|
+
exists: true,
|
|
378
|
+
retention: getChannelRetention(channelId),
|
|
379
|
+
require_identity: getChannelRequireIdentity(channelId),
|
|
380
|
+
trust_mode: getChannelTrustMode(channelId),
|
|
381
|
+
session_ttl_seconds: Math.round(getChannelSessionTtlMs(channelId) / 1000),
|
|
382
|
+
is_band: getChannelIsBand(channelId),
|
|
383
|
+
agent_count: getOrCreateChannel(channelId).size(),
|
|
384
|
+
endpoints: {
|
|
385
|
+
join: `POST ${base}/join`,
|
|
386
|
+
send: `POST ${base}/send`,
|
|
387
|
+
listen: `GET ${base}/listen?timeout=30`,
|
|
388
|
+
roster: `GET ${base}/roster`,
|
|
389
|
+
history: `GET ${base}/history?n=20`,
|
|
390
|
+
leave: `POST ${base}/leave`,
|
|
391
|
+
keepalive: `POST ${base}/keepalive`,
|
|
392
|
+
stats: `GET ${base}/stats`,
|
|
393
|
+
transcript: `GET ${base}/transcript`,
|
|
394
|
+
webhooks: `POST ${base}/webhooks`,
|
|
395
|
+
mcp: `${opts.publicOrigin}/mcp/${channelId}`,
|
|
396
|
+
},
|
|
397
|
+
auth: "All endpoints (except this one) require Authorization: Bearer <channel_token>. /send and /listen also require X-Session-Id from /join.",
|
|
398
|
+
docs: `${opts.publicOrigin}/llms.txt`,
|
|
399
|
+
});
|
|
400
|
+
});
|
|
366
401
|
app.get("/api/channels/:channelId/transcript", (c) => {
|
|
367
402
|
const channelId = c.req.param("channelId");
|
|
368
403
|
if (!channelExists(channelId))
|
|
@@ -496,10 +531,11 @@ export function createApp(opts) {
|
|
|
496
531
|
if (!resolvedCallsign)
|
|
497
532
|
return c.json({ error: "callsign or identity_key required", code: "invalid" }, 400);
|
|
498
533
|
const incoming = c.req.header("x-session-id") ?? c.req.header("X-Session-Id");
|
|
499
|
-
const
|
|
534
|
+
const selfGenerated = !(incoming && incoming.length >= 8);
|
|
535
|
+
const newId = selfGenerated ? randomUUID() : incoming;
|
|
500
536
|
const channel = getOrCreateChannel(channelId);
|
|
501
537
|
try {
|
|
502
|
-
const result = channel.join(newId, resolvedCallsign);
|
|
538
|
+
const result = channel.join(newId, resolvedCallsign, { selfGenerated });
|
|
503
539
|
if (!result.idempotent) {
|
|
504
540
|
statsRecordJoin();
|
|
505
541
|
transcriptRecordJoin(channelId, getChannelRetention(channelId), resolvedCallsign);
|
package/dist/channel.js
CHANGED
|
@@ -62,16 +62,16 @@ export class Channel {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Idempotent join.
|
|
65
|
-
* -
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
65
|
+
* - Same `(sessionId, callsign)` is a no-op (refreshes lastSeen).
|
|
66
|
+
* - `opts.selfGenerated=true` means the caller has no prior identity (REST minted a UUID for
|
|
67
|
+
* them); if the callsign is already taken, we return the existing session_id so the caller
|
|
68
|
+
* can adopt it. Enables the "defensively re-join every turn" pattern.
|
|
69
|
+
* - `opts.selfGenerated=false` (default) means the caller's sessionId IS their identity (MCP
|
|
70
|
+
* Mcp-Session-Id, or REST with X-Session-Id). If the callsign is taken by a *different*
|
|
71
|
+
* session, throws `callsign_taken` (409) rather than silently mapping them to someone
|
|
72
|
+
* else's session. Previous behavior silently broke send/listen for the conflicting caller.
|
|
73
73
|
*/
|
|
74
|
-
join(sessionId, callsign) {
|
|
74
|
+
join(sessionId, callsign, opts = {}) {
|
|
75
75
|
const normalized = callsign.trim().toLowerCase();
|
|
76
76
|
if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(normalized)) {
|
|
77
77
|
throw new ChannelError("callsign must be 1-32 chars, alphanumeric/underscore/dash, starting with letter or digit", "invalid", 400);
|
|
@@ -81,29 +81,25 @@ export class Channel {
|
|
|
81
81
|
}
|
|
82
82
|
const existingSession = this.sessionByCallsign.get(normalized);
|
|
83
83
|
let idempotent = false;
|
|
84
|
-
|
|
84
|
+
const effectiveId = sessionId;
|
|
85
85
|
if (existingSession) {
|
|
86
86
|
if (existingSession === sessionId) {
|
|
87
87
|
idempotent = true;
|
|
88
88
|
}
|
|
89
|
+
else if (opts.selfGenerated) {
|
|
90
|
+
// Caller had no identity (REST minted a UUID for them) and the callsign is taken —
|
|
91
|
+
// hand back the existing session_id so they can adopt it.
|
|
92
|
+
this.evictedSessions.delete(sessionId);
|
|
93
|
+
this.touch(existingSession);
|
|
94
|
+
return {
|
|
95
|
+
sessionId: existingSession,
|
|
96
|
+
roster: this.roster(),
|
|
97
|
+
history: this.history(20),
|
|
98
|
+
idempotent: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
89
101
|
else {
|
|
90
|
-
|
|
91
|
-
// than evicting; we infer REST mode by an unseen sessionId AND existing callsign holder.
|
|
92
|
-
// For MCP, the transport's sticky Mcp-Session-Id means existingSession === sessionId for
|
|
93
|
-
// the same client; a different client trying to take the callsign passes a different id
|
|
94
|
-
// and will reach the else branch below.
|
|
95
|
-
if (!this.callsignBySession.has(sessionId)) {
|
|
96
|
-
// Reuse existing — idempotent rejoin (the common turn-based-agent case)
|
|
97
|
-
this.evictedSessions.delete(sessionId);
|
|
98
|
-
this.touch(existingSession);
|
|
99
|
-
return {
|
|
100
|
-
sessionId: existingSession,
|
|
101
|
-
roster: this.roster(),
|
|
102
|
-
history: this.history(20),
|
|
103
|
-
idempotent: true,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
this.evictSession(existingSession);
|
|
102
|
+
throw new ChannelError(`callsign "${normalized}" is already in use on this channel; pick a different one or have the current holder leave first`, "callsign_taken", 409);
|
|
107
103
|
}
|
|
108
104
|
}
|
|
109
105
|
const prevCallsign = this.callsignBySession.get(sessionId);
|
package/dist/landing.js
CHANGED
|
@@ -123,7 +123,7 @@ export function landingHtml() {
|
|
|
123
123
|
.panel { display: none; padding-top: 12px; }
|
|
124
124
|
.panel[aria-current="true"] { display: block; }
|
|
125
125
|
.panel p { color: var(--dim); font-size: 13px; margin: 0 0 10px; }
|
|
126
|
-
pre
|
|
126
|
+
pre {
|
|
127
127
|
font-family: inherit;
|
|
128
128
|
background: var(--bg);
|
|
129
129
|
border: 1px solid var(--line);
|
|
@@ -131,8 +131,20 @@ export function landingHtml() {
|
|
|
131
131
|
overflow-x: auto;
|
|
132
132
|
font-size: 13px;
|
|
133
133
|
user-select: all;
|
|
134
|
+
margin: 0;
|
|
135
|
+
white-space: pre-wrap;
|
|
136
|
+
word-break: break-all;
|
|
137
|
+
line-height: 1.45;
|
|
138
|
+
}
|
|
139
|
+
code {
|
|
140
|
+
font-family: inherit;
|
|
141
|
+
background: var(--bg);
|
|
142
|
+
border: 1px solid var(--line);
|
|
143
|
+
padding: 1px 6px;
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
user-select: all;
|
|
134
146
|
}
|
|
135
|
-
pre {
|
|
147
|
+
pre code { background: none; border: none; padding: 0; font-size: inherit; }
|
|
136
148
|
.copy { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
|
137
149
|
h2 { font-size: 22px; letter-spacing: -0.02em; margin: 56px 0 16px; }
|
|
138
150
|
ol { padding-left: 20px; }
|
package/package.json
CHANGED