@saadnvd1/agent-os 0.3.770 → 0.3.777
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/app/api/live-shares/[id]/route.ts +72 -0
- package/app/api/live-shares/route.test.ts +45 -0
- package/app/api/live-shares/route.ts +40 -0
- package/app/api/sessions/[id]/artifacts/[artifactId]/compare/route.ts +41 -0
- package/app/api/shares/preview/route.ts +6 -1
- package/app/api/shares/route.test.ts +51 -0
- package/app/page.tsx +6 -0
- package/components/Artifacts/ArtifactActions.tsx +27 -3
- package/components/Artifacts/ArtifactPreview.tsx +27 -12
- package/components/Artifacts/Compare/DiffView.tsx +116 -0
- package/components/Artifacts/Compare/SideBySide.tsx +60 -0
- package/components/Artifacts/Compare/VersionPicker.tsx +47 -0
- package/components/Artifacts/Compare/index.tsx +191 -0
- package/components/Chat/ChatPanel.tsx +6 -0
- package/components/Chat/EventRow.tsx +6 -0
- package/components/Chat/Messages.tsx +6 -0
- package/components/Chat/ShareFromHere.tsx +39 -0
- package/components/CommandPalette/CommandPalette.tsx +5 -4
- package/components/CommandPalette/CommandsBody.tsx +97 -131
- package/components/CommandPalette/PaletteRows.tsx +79 -0
- package/components/CommandPalette/PaletteTabs.tsx +51 -0
- package/components/CommandPalette/conversations/ConversationSearch.tsx +13 -11
- package/components/CommandPalette/useAppCommands.ts +14 -0
- package/components/CommandPalette/useConversationRows.ts +55 -0
- package/components/Live/GuestPromptCard.tsx +64 -0
- package/components/Live/InviteDialog.tsx +225 -0
- package/components/Live/LiveChip.tsx +109 -0
- package/components/Live/LivePanel.tsx +81 -0
- package/components/Live/LobbyCard.tsx +64 -0
- package/components/Pane/MobileTabBar.tsx +8 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/SessionRowMenu.tsx +6 -0
- package/components/SessionList/useSessionSwitching.ts +2 -2
- package/components/Share/MessageList.tsx +87 -0
- package/components/Share/RangePicker.tsx +59 -0
- package/components/Share/ShareDialog.tsx +113 -52
- package/components/Shares/SharedLinksPanel.tsx +51 -0
- package/data/live/index.ts +2 -0
- package/data/live/keys.ts +4 -0
- package/data/live/queries.ts +81 -0
- package/data/session-artifacts/keys.ts +2 -0
- package/data/session-artifacts/queries.ts +25 -0
- package/data/shares/queries.ts +11 -0
- package/hooks/usePaletteCommands.ts +4 -1
- package/lib/artifacts/actions.ts +11 -1
- package/lib/artifacts/compare/compareVersions.ts +66 -0
- package/lib/artifacts/compare/diff.ts +178 -0
- package/lib/artifacts/compare/extractText.ts +64 -0
- package/lib/artifacts/compare/index.ts +11 -0
- package/lib/artifacts/compare/types.ts +44 -0
- package/lib/artifacts/match.ts +64 -0
- package/lib/artifacts/store.ts +39 -10
- package/lib/artifacts/tools.ts +9 -4
- package/lib/artifacts/versions.ts +2 -1
- package/lib/chat/events.ts +4 -1
- package/lib/chat/registry.ts +10 -0
- package/lib/connect/control-api.ts +44 -0
- package/lib/db/migrations/20261029T1400_live_shares.ts +42 -0
- package/lib/live/clean.ts +54 -0
- package/lib/live/frames.ts +132 -0
- package/lib/live/inbox.ts +225 -0
- package/lib/live/publisher.ts +84 -0
- package/lib/live/ring.ts +61 -0
- package/lib/live/runtime.ts +51 -0
- package/lib/live/service.ts +318 -0
- package/lib/live/socket.ts +293 -0
- package/lib/live/store.ts +149 -0
- package/lib/live/types.ts +39 -0
- package/lib/palette/registry.ts +71 -17
- package/lib/palette/sections.ts +143 -0
- package/lib/palette/tabs.ts +21 -0
- package/lib/palette/usage.ts +81 -0
- package/lib/security/demo.ts +3 -0
- package/lib/share/connect.ts +24 -0
- package/lib/share/markdown.ts +3 -0
- package/lib/share/range.ts +189 -0
- package/lib/share/redact.ts +23 -1
- package/lib/share/render.tsx +3 -0
- package/lib/share/secret-patterns.ts +77 -0
- package/lib/share/service.ts +64 -19
- package/lib/share/snapshot.ts +39 -2
- package/lib/share/styles.ts +2 -0
- package/lib/share/view.ts +11 -0
- package/lib/sidebar/row-menu.ts +2 -0
- package/package.json +1 -1
- package/server.ts +2 -0
- package/stores/liveUi.ts +17 -0
- package/stores/palette.ts +13 -11
- package/stores/shareUi.ts +6 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Connect sends down. Only the types below are accepted, and only for a
|
|
3
|
+
* share this machine minted and still holds. Everything else is dropped and
|
|
4
|
+
* counted: nothing downward is executed, interpreted or sent on to an agent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { cleanBody, cleanName } from "./clean";
|
|
9
|
+
import { chunk } from "./ring";
|
|
10
|
+
import {
|
|
11
|
+
admittedGuestIds,
|
|
12
|
+
hasJoinRequest,
|
|
13
|
+
hasSuggestion,
|
|
14
|
+
logLive,
|
|
15
|
+
markLiveEnded,
|
|
16
|
+
openSuggestions,
|
|
17
|
+
} from "./store";
|
|
18
|
+
import { MAX_LOBBY, MAX_PENDING_CARDS, live, type Runtime } from "./runtime";
|
|
19
|
+
|
|
20
|
+
export const DOWNWARD = [
|
|
21
|
+
"roster",
|
|
22
|
+
"join_request",
|
|
23
|
+
"presence",
|
|
24
|
+
"suggestion",
|
|
25
|
+
"guest_left",
|
|
26
|
+
"ended",
|
|
27
|
+
"slow_down",
|
|
28
|
+
"invalid",
|
|
29
|
+
"admit_failed",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
const id = z.string().min(1).max(64);
|
|
33
|
+
const guest = z.object({ id, name: z.string().max(400) });
|
|
34
|
+
|
|
35
|
+
const SCHEMAS = {
|
|
36
|
+
roster: z.object({
|
|
37
|
+
guests: z.array(guest.extend({ status: z.string().max(32) })).max(100),
|
|
38
|
+
}),
|
|
39
|
+
join_request: z.object({ guest }),
|
|
40
|
+
presence: z.object({ guest, online: z.boolean() }),
|
|
41
|
+
suggestion: z.object({ id, guest, text: z.string().max(8000) }),
|
|
42
|
+
guest_left: z.object({ guest }),
|
|
43
|
+
ended: z.object({}),
|
|
44
|
+
slow_down: z.object({}),
|
|
45
|
+
invalid: z.object({}),
|
|
46
|
+
admit_failed: z.object({ guest_id: id.optional(), reason: z.string() }),
|
|
47
|
+
} satisfies Record<(typeof DOWNWARD)[number], z.ZodType>;
|
|
48
|
+
|
|
49
|
+
export interface InboxEffects {
|
|
50
|
+
send: (
|
|
51
|
+
shareId: string,
|
|
52
|
+
action: string,
|
|
53
|
+
data?: Record<string, unknown>
|
|
54
|
+
) => void;
|
|
55
|
+
/** The share is over: stop publishing and drop the subscription. */
|
|
56
|
+
ended: (shareId: string) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dropped = { count: 0 };
|
|
60
|
+
export const droppedCount = () => dropped.count;
|
|
61
|
+
|
|
62
|
+
function drop(why: string) {
|
|
63
|
+
dropped.count++;
|
|
64
|
+
console.warn(`live: dropped a frame from Connect (${why})`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Handles one frame for the share it arrived on. Returns whether it was used. */
|
|
68
|
+
export function handleFrame(
|
|
69
|
+
shareId: string,
|
|
70
|
+
frame: Record<string, unknown>,
|
|
71
|
+
fx: InboxEffects,
|
|
72
|
+
now = Date.now()
|
|
73
|
+
): boolean {
|
|
74
|
+
const rt = live.runtimes.get(shareId);
|
|
75
|
+
if (!rt || rt.share.ended_at) return (drop("unknown share"), false);
|
|
76
|
+
const type = frame.type;
|
|
77
|
+
if (
|
|
78
|
+
typeof type !== "string" ||
|
|
79
|
+
!(DOWNWARD as readonly string[]).includes(type)
|
|
80
|
+
)
|
|
81
|
+
return (drop("type not allowed"), false);
|
|
82
|
+
if (frame.share_id !== undefined && frame.share_id !== shareId)
|
|
83
|
+
return (drop("another share"), false);
|
|
84
|
+
const parsed = SCHEMAS[type as keyof typeof SCHEMAS].safeParse(frame);
|
|
85
|
+
if (!parsed.success) return (drop(`bad ${type}`), false);
|
|
86
|
+
|
|
87
|
+
switch (type) {
|
|
88
|
+
case "roster": {
|
|
89
|
+
const admitted = admittedGuestIds(shareId);
|
|
90
|
+
for (const g of (parsed.data as z.infer<typeof SCHEMAS.roster>).guests) {
|
|
91
|
+
const name = cleanName(g.name);
|
|
92
|
+
if (!name) continue;
|
|
93
|
+
if (admitted.has(g.id)) setGuest(rt, g.id, name, "admitted", now);
|
|
94
|
+
else if (g.status === "pending") {
|
|
95
|
+
if (!rt.guests.has(g.id) && pendingCount(rt) >= MAX_LOBBY) continue;
|
|
96
|
+
setGuest(rt, g.id, name, "pending", now);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
case "join_request": {
|
|
102
|
+
const g = (parsed.data as z.infer<typeof SCHEMAS.join_request>).guest;
|
|
103
|
+
const name = cleanName(g.name);
|
|
104
|
+
if (!name) return (drop("bad name"), false);
|
|
105
|
+
if (rt.guests.has(g.id)) return true;
|
|
106
|
+
if (pendingCount(rt) >= MAX_LOBBY) {
|
|
107
|
+
fx.send(shareId, "deny", { guest_id: g.id });
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
setGuest(rt, g.id, name, "pending", now);
|
|
111
|
+
if (!hasJoinRequest(shareId, g.id))
|
|
112
|
+
logLive({
|
|
113
|
+
share_id: shareId,
|
|
114
|
+
kind: "join_request",
|
|
115
|
+
guest_id: g.id,
|
|
116
|
+
guest_name: name,
|
|
117
|
+
ref: null,
|
|
118
|
+
text: null,
|
|
119
|
+
sent_text: null,
|
|
120
|
+
});
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
case "presence": {
|
|
124
|
+
const p = parsed.data as z.infer<typeof SCHEMAS.presence>;
|
|
125
|
+
const known = rt.guests.get(p.guest.id);
|
|
126
|
+
if (!known || known.status !== "admitted") return true;
|
|
127
|
+
known.online = p.online;
|
|
128
|
+
if (p.online) {
|
|
129
|
+
const frames = chunk(rt.ring.list());
|
|
130
|
+
for (const items of frames)
|
|
131
|
+
fx.send(shareId, "catchup", { guest_id: known.id, items });
|
|
132
|
+
if (!frames.length) return true;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
case "guest_left": {
|
|
137
|
+
const g = (parsed.data as z.infer<typeof SCHEMAS.guest_left>).guest;
|
|
138
|
+
const known = rt.guests.get(g.id);
|
|
139
|
+
if (!known) return true;
|
|
140
|
+
if (known.status === "pending") rt.guests.delete(g.id);
|
|
141
|
+
else known.online = false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
case "suggestion": {
|
|
145
|
+
const s = parsed.data as z.infer<typeof SCHEMAS.suggestion>;
|
|
146
|
+
const known = rt.guests.get(s.guest.id);
|
|
147
|
+
// Only a guest this machine let in can leave a card.
|
|
148
|
+
if (!known || known.status !== "admitted")
|
|
149
|
+
return (drop("suggestion from a guest not admitted here"), false);
|
|
150
|
+
const text = cleanBody(s.text);
|
|
151
|
+
if (!text) return (drop("empty suggestion"), false);
|
|
152
|
+
if (hasSuggestion(shareId, s.id))
|
|
153
|
+
return (drop("duplicate suggestion"), false);
|
|
154
|
+
if (openSuggestions(shareId).length >= MAX_PENDING_CARDS) {
|
|
155
|
+
fx.send(shareId, "suggestion_status", {
|
|
156
|
+
guest_id: known.id,
|
|
157
|
+
id: s.id,
|
|
158
|
+
status: "dismissed",
|
|
159
|
+
});
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
logLive({
|
|
163
|
+
share_id: shareId,
|
|
164
|
+
kind: "suggestion",
|
|
165
|
+
guest_id: known.id,
|
|
166
|
+
guest_name: known.name,
|
|
167
|
+
ref: s.id,
|
|
168
|
+
text,
|
|
169
|
+
sent_text: null,
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
case "ended":
|
|
174
|
+
markLiveEnded(shareId);
|
|
175
|
+
fx.ended(shareId);
|
|
176
|
+
return true;
|
|
177
|
+
case "slow_down":
|
|
178
|
+
rt.slowUntil = now + 5000;
|
|
179
|
+
return true;
|
|
180
|
+
case "admit_failed": {
|
|
181
|
+
const gid = (parsed.data as z.infer<typeof SCHEMAS.admit_failed>)
|
|
182
|
+
.guest_id;
|
|
183
|
+
const g = gid ? rt.guests.get(gid) : undefined;
|
|
184
|
+
if (g) g.status = "pending";
|
|
185
|
+
// The admit line must not outlive a refused admit.
|
|
186
|
+
if (gid && admittedGuestIds(shareId).has(gid))
|
|
187
|
+
logLive({
|
|
188
|
+
share_id: shareId,
|
|
189
|
+
kind: "deny",
|
|
190
|
+
guest_id: gid,
|
|
191
|
+
guest_name: g?.name ?? null,
|
|
192
|
+
ref: null,
|
|
193
|
+
text: null,
|
|
194
|
+
sent_text: null,
|
|
195
|
+
});
|
|
196
|
+
rt.notice = "Your plan's guest limit is reached.";
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
case "invalid":
|
|
200
|
+
rt.drops++;
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function pendingCount(rt: Runtime) {
|
|
207
|
+
return [...rt.guests.values()].filter((x) => x.status === "pending").length;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function setGuest(
|
|
211
|
+
rt: Runtime,
|
|
212
|
+
id: string,
|
|
213
|
+
name: string,
|
|
214
|
+
status: "pending" | "admitted",
|
|
215
|
+
at: number
|
|
216
|
+
) {
|
|
217
|
+
const old = rt.guests.get(id);
|
|
218
|
+
rt.guests.set(id, {
|
|
219
|
+
id,
|
|
220
|
+
name,
|
|
221
|
+
status,
|
|
222
|
+
online: old?.online ?? false,
|
|
223
|
+
at: old?.at ?? at,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat messages of a live session, as frames for Connect. Registered as a tap
|
|
3
|
+
* beside emit() in the chat registry, only for sessions with a live share.
|
|
4
|
+
* An assistant turn is redacted whole and sent at most once a second.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ChatItem, ChatServerMessage } from "../chat/events";
|
|
8
|
+
import { listItems } from "../chat/store";
|
|
9
|
+
import { visible } from "../share/snapshot";
|
|
10
|
+
import { stateOf } from "./frames";
|
|
11
|
+
import { live, type Runtime } from "./runtime";
|
|
12
|
+
|
|
13
|
+
const FLUSH_MS = 1000;
|
|
14
|
+
const SLOW_FLUSH_MS = 4000;
|
|
15
|
+
|
|
16
|
+
export function seed(rt: Runtime, sessionId: string): void {
|
|
17
|
+
for (const item of visible(listItems(sessionId))) {
|
|
18
|
+
const frame = rt.map(item);
|
|
19
|
+
if (frame) rt.ring.upsert(frame);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function flush(rt: Runtime): void {
|
|
24
|
+
if (rt.flushTimer) clearTimeout(rt.flushTimer);
|
|
25
|
+
rt.flushTimer = null;
|
|
26
|
+
const batch = [...rt.dirty.values()];
|
|
27
|
+
rt.dirty.clear();
|
|
28
|
+
for (const { item, text } of batch) {
|
|
29
|
+
// A word still being typed is held back: half a key isn't recognisable.
|
|
30
|
+
const open = text !== undefined && rt.streaming.has(item.id);
|
|
31
|
+
const frame = rt.map(item, open ? text.replace(/\S+$/, "") : text);
|
|
32
|
+
if (!frame) continue;
|
|
33
|
+
rt.ring.upsert(frame);
|
|
34
|
+
live.socket?.send(rt.share.id, "item", frame);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function schedule(rt: Runtime, now: boolean) {
|
|
39
|
+
if (now) return flush(rt);
|
|
40
|
+
if (rt.flushTimer) return;
|
|
41
|
+
rt.flushTimer = setTimeout(
|
|
42
|
+
() => flush(rt),
|
|
43
|
+
Date.now() < rt.slowUntil ? SLOW_FLUSH_MS : FLUSH_MS
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mark(rt: Runtime, item: ChatItem, text?: string) {
|
|
48
|
+
rt.dirty.set(item.id, { item, text });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function onMessage(sessionId: string, m: ChatServerMessage): void {
|
|
52
|
+
for (const rt of live.runtimes.values()) {
|
|
53
|
+
if (rt.share.session_id !== sessionId || rt.share.ended_at) continue;
|
|
54
|
+
if (m.type === "state") {
|
|
55
|
+
live.socket?.send(rt.share.id, "state", { state: stateOf(m.state) });
|
|
56
|
+
} else if (m.type === "item") {
|
|
57
|
+
const item = m.item;
|
|
58
|
+
if (item.kind === "turn_end") {
|
|
59
|
+
flush(rt);
|
|
60
|
+
live.socket?.send(rt.share.id, "state", { state: "idle" });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const streaming =
|
|
64
|
+
(item.kind === "assistant" || item.kind === "reasoning") &&
|
|
65
|
+
item.streaming === true;
|
|
66
|
+
if (item.kind === "assistant") {
|
|
67
|
+
if (streaming) rt.streaming.set(item.id, { item, text: item.text });
|
|
68
|
+
else rt.streaming.delete(item.id);
|
|
69
|
+
}
|
|
70
|
+
mark(
|
|
71
|
+
rt,
|
|
72
|
+
item,
|
|
73
|
+
item.kind === "assistant" && streaming ? item.text : undefined
|
|
74
|
+
);
|
|
75
|
+
schedule(rt, !streaming);
|
|
76
|
+
} else if (m.type === "delta") {
|
|
77
|
+
const cur = rt.streaming.get(m.id);
|
|
78
|
+
if (!cur) continue;
|
|
79
|
+
cur.text += m.text;
|
|
80
|
+
mark(rt, cur.item, cur.text);
|
|
81
|
+
schedule(rt, false);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/lib/live/ring.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ItemFrame } from "./frames";
|
|
2
|
+
|
|
3
|
+
export const RING_ITEMS = 200;
|
|
4
|
+
export const RING_BYTES = 512 * 1024;
|
|
5
|
+
|
|
6
|
+
const sizeOf = (f: ItemFrame) => Buffer.byteLength(JSON.stringify(f));
|
|
7
|
+
|
|
8
|
+
/** The last items of a share, already redacted, for guests who join late. */
|
|
9
|
+
export class Ring {
|
|
10
|
+
private items = new Map<string, { frame: ItemFrame; size: number }>();
|
|
11
|
+
private bytes = 0;
|
|
12
|
+
|
|
13
|
+
upsert(frame: ItemFrame): void {
|
|
14
|
+
const old = this.items.get(frame.id);
|
|
15
|
+
if (old) this.bytes -= old.size;
|
|
16
|
+
const size = sizeOf(frame);
|
|
17
|
+
// Same slot when it already exists, so an upsert doesn't reorder the chat.
|
|
18
|
+
this.items.set(frame.id, { frame, size });
|
|
19
|
+
this.bytes += size;
|
|
20
|
+
while (this.items.size > RING_ITEMS || this.bytes > RING_BYTES) {
|
|
21
|
+
const first = this.items.keys().next().value;
|
|
22
|
+
if (first === undefined) break;
|
|
23
|
+
this.bytes -= this.items.get(first)!.size;
|
|
24
|
+
this.items.delete(first);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
list(): ItemFrame[] {
|
|
29
|
+
return [...this.items.values()].map((v) => v.frame);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get size() {
|
|
33
|
+
return this.items.size;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get byteSize() {
|
|
37
|
+
return this.bytes;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Catch-up frames: at most 200 items and `maxBytes` each, in order. */
|
|
42
|
+
export function chunk(
|
|
43
|
+
frames: ItemFrame[],
|
|
44
|
+
maxBytes = 48 * 1024
|
|
45
|
+
): ItemFrame[][] {
|
|
46
|
+
const out: ItemFrame[][] = [];
|
|
47
|
+
let cur: ItemFrame[] = [];
|
|
48
|
+
let size = 0;
|
|
49
|
+
for (const f of frames) {
|
|
50
|
+
const n = sizeOf(f);
|
|
51
|
+
if (cur.length && (size + n > maxBytes || cur.length >= RING_ITEMS)) {
|
|
52
|
+
out.push(cur);
|
|
53
|
+
cur = [];
|
|
54
|
+
size = 0;
|
|
55
|
+
}
|
|
56
|
+
cur.push(f);
|
|
57
|
+
size += n;
|
|
58
|
+
}
|
|
59
|
+
if (cur.length) out.push(cur);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ChatItem } from "../chat/events";
|
|
2
|
+
import type { ShareContext } from "../share/redact";
|
|
3
|
+
import type { LiveShareRow } from "./store";
|
|
4
|
+
import type { ItemMapper } from "./frames";
|
|
5
|
+
import { Ring } from "./ring";
|
|
6
|
+
import type { LiveSocket } from "./socket";
|
|
7
|
+
|
|
8
|
+
export interface Guest {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
/** pending: waiting in the lobby. admitted: let in by this machine. */
|
|
12
|
+
status: "pending" | "admitted";
|
|
13
|
+
online: boolean;
|
|
14
|
+
/** When the request arrived, for the 10-minute lobby expiry. */
|
|
15
|
+
at: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What one live share holds in memory; everything durable is in the log. */
|
|
19
|
+
export interface Runtime {
|
|
20
|
+
share: LiveShareRow;
|
|
21
|
+
guests: Map<string, Guest>;
|
|
22
|
+
ring: Ring;
|
|
23
|
+
ctx: ShareContext;
|
|
24
|
+
map: ItemMapper;
|
|
25
|
+
/** Accumulated text of assistant items still streaming, by item id. */
|
|
26
|
+
streaming: Map<string, { item: ChatItem; text: string }>;
|
|
27
|
+
/** Items waiting for the next flush, by id (latest wins). */
|
|
28
|
+
dirty: Map<string, { item: ChatItem; text?: string }>;
|
|
29
|
+
flushTimer: ReturnType<typeof setTimeout> | null;
|
|
30
|
+
slowUntil: number;
|
|
31
|
+
notice: string | null;
|
|
32
|
+
drops: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LiveGlobals {
|
|
36
|
+
runtimes: Map<string, Runtime>;
|
|
37
|
+
socket: LiveSocket | null;
|
|
38
|
+
started: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// One per process, shared by the custom server and the route bundles.
|
|
42
|
+
const g = globalThis as unknown as { __agentosLive?: LiveGlobals };
|
|
43
|
+
export const live: LiveGlobals = (g.__agentosLive ??= {
|
|
44
|
+
runtimes: new Map(),
|
|
45
|
+
socket: null,
|
|
46
|
+
started: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const LOBBY_MS = 10 * 60_000;
|
|
50
|
+
export const MAX_PENDING_CARDS = 20;
|
|
51
|
+
export const MAX_LOBBY = 10;
|