relay-companion 0.1.0
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/bin/relay.js +262 -0
- package/overlay/inbox.html +1398 -0
- package/overlay/main.cjs +762 -0
- package/overlay/preload.cjs +37 -0
- package/overlay/sounds/tink.wav +0 -0
- package/package.json +25 -0
- package/src/claude-materializer.js +85 -0
- package/src/claude-session-writer.js +629 -0
- package/src/client.js +168 -0
- package/src/codex-app-server.js +120 -0
- package/src/codex-desktop.js +276 -0
- package/src/codex-session-writer.js +170 -0
- package/src/codex-state.js +114 -0
- package/src/config.js +62 -0
- package/src/host-json.js +14 -0
- package/src/host-paths.js +67 -0
- package/src/install.js +142 -0
- package/src/materializer.js +378 -0
- package/src/mcp.js +419 -0
- package/src/notifications.js +412 -0
- package/src/pinning.js +43 -0
- package/src/relay-briefing.js +344 -0
- package/src/runtime.js +1141 -0
- package/src/task-daemon.js +216 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { RelayClient } from "./client.js";
|
|
3
|
+
import {
|
|
4
|
+
ensureRuntimeSession,
|
|
5
|
+
freshMessages,
|
|
6
|
+
markMessagesProcessed,
|
|
7
|
+
orderTaskMessages,
|
|
8
|
+
readTaskLedger,
|
|
9
|
+
writeTaskLedger,
|
|
10
|
+
} from "./runtime.js";
|
|
11
|
+
import {
|
|
12
|
+
buildHumanNotifications,
|
|
13
|
+
defaultStageRelayCompanionItem,
|
|
14
|
+
freshNotifications,
|
|
15
|
+
markNotificationsProcessed,
|
|
16
|
+
} from "./notifications.js";
|
|
17
|
+
|
|
18
|
+
function idempotencyKey(prefix) {
|
|
19
|
+
return `${prefix}_${randomUUID()}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function messagesForSession(session, messages) {
|
|
23
|
+
return messages.filter((message) => message.taskId === session.taskId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function markProcessedOnce(ledger, processed, messages) {
|
|
27
|
+
const unseen = [];
|
|
28
|
+
for (const message of messages) {
|
|
29
|
+
if (processed.has(message.id)) continue;
|
|
30
|
+
processed.add(message.id);
|
|
31
|
+
unseen.push(message);
|
|
32
|
+
}
|
|
33
|
+
if (unseen.length) markMessagesProcessed(ledger, unseen);
|
|
34
|
+
return unseen;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function freshTaskEvents(ledger, events) {
|
|
38
|
+
ledger.taskEvents ||= {};
|
|
39
|
+
return events.filter((event) => {
|
|
40
|
+
const seen = ledger.taskEvents[event.id];
|
|
41
|
+
return !seen || seen.sequence !== event.sequence;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function markTaskEventsProcessed(ledger, events) {
|
|
46
|
+
ledger.taskEvents ||= {};
|
|
47
|
+
const processedAt = new Date().toISOString();
|
|
48
|
+
for (const event of events) {
|
|
49
|
+
ledger.taskEvents[event.id] = {
|
|
50
|
+
taskId: event.taskId,
|
|
51
|
+
sequence: event.sequence,
|
|
52
|
+
type: event.type,
|
|
53
|
+
processedAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function pollVisibleTaskEvents({ client, ledger, tasks, log }) {
|
|
59
|
+
if (typeof client.taskEvents !== "function") return [];
|
|
60
|
+
const visibleEvents = [];
|
|
61
|
+
for (const task of tasks.tasks || []) {
|
|
62
|
+
try {
|
|
63
|
+
const result = await client.taskEvents(task.id);
|
|
64
|
+
const fresh = freshTaskEvents(ledger, result.events || []);
|
|
65
|
+
if (fresh.length) {
|
|
66
|
+
markTaskEventsProcessed(ledger, fresh);
|
|
67
|
+
for (const event of fresh) {
|
|
68
|
+
log(`observed task event ${event.id} (${event.type}) for task ${task.id}`);
|
|
69
|
+
}
|
|
70
|
+
visibleEvents.push(...fresh);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log(`task event polling failed for ${task.id}: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return visibleEvents;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function pollHumanNotifications({ client, ledger, stageCompanionItem, log }) {
|
|
80
|
+
try {
|
|
81
|
+
const [me, tasks, relays, connectors] = await Promise.all([
|
|
82
|
+
client.me(),
|
|
83
|
+
client.listTasks(),
|
|
84
|
+
client.listRelays(),
|
|
85
|
+
client.listConnectors(),
|
|
86
|
+
]);
|
|
87
|
+
const events = await pollVisibleTaskEvents({ client, ledger, tasks, log });
|
|
88
|
+
const notifications = buildHumanNotifications({
|
|
89
|
+
user: me.user,
|
|
90
|
+
tasks: tasks.tasks || [],
|
|
91
|
+
relays: relays.relays || [],
|
|
92
|
+
connectors: connectors.connectors || [],
|
|
93
|
+
});
|
|
94
|
+
const fresh = freshNotifications(ledger, notifications);
|
|
95
|
+
const staged = [];
|
|
96
|
+
for (const notification of fresh) {
|
|
97
|
+
try {
|
|
98
|
+
stageCompanionItem(notification);
|
|
99
|
+
staged.push(notification);
|
|
100
|
+
log(`staged Relay companion item ${notification.kind}: ${notification.title}`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
log(`Relay companion staging failed for ${notification.kind}: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (staged.length) markNotificationsProcessed(ledger, staged);
|
|
106
|
+
return { notifications: staged, events };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
log(`Relay companion attention polling failed: ${err.message}`);
|
|
109
|
+
return { notifications: [], events: [] };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function pollTaskRuntimeOnce({
|
|
114
|
+
client = new RelayClient(),
|
|
115
|
+
log = () => {},
|
|
116
|
+
stageCompanionItem = defaultStageRelayCompanionItem,
|
|
117
|
+
adapters,
|
|
118
|
+
} = {}) {
|
|
119
|
+
const ledger = readTaskLedger();
|
|
120
|
+
const inbox = await client.agentInbox();
|
|
121
|
+
const fresh = freshMessages(ledger, inbox.messages || []);
|
|
122
|
+
const processedMessageIds = new Set();
|
|
123
|
+
const processedMessages = [];
|
|
124
|
+
const touched = [];
|
|
125
|
+
|
|
126
|
+
for (const session of inbox.sessions || []) {
|
|
127
|
+
let claimedSession = session;
|
|
128
|
+
try {
|
|
129
|
+
const claimState = session.state === "queued" || session.state === "idle" || session.state === "stale"
|
|
130
|
+
? "starting"
|
|
131
|
+
: session.state;
|
|
132
|
+
const claimed = await client.heartbeatSession(session.id, {
|
|
133
|
+
state: claimState,
|
|
134
|
+
sessionRef: session.sessionRef || {},
|
|
135
|
+
lastError: null,
|
|
136
|
+
idempotencyKey: idempotencyKey("claim"),
|
|
137
|
+
});
|
|
138
|
+
claimedSession = claimed.session || session;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err.status === 409 || err.body?.error === "version_conflict") {
|
|
141
|
+
log(`session ${session.id} is leased by another companion; skipping`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
log(`claim failed for ${session.id}: ${err.message}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const sessionMessages = orderTaskMessages(messagesForSession(claimedSession, fresh));
|
|
148
|
+
let runtime;
|
|
149
|
+
try {
|
|
150
|
+
runtime = await ensureRuntimeSession({ session: claimedSession, messages: sessionMessages, ledger, adapters });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
log(`runtime failed for ${session.id}: ${err.message}`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
processedMessages.push(...markProcessedOnce(ledger, processedMessageIds, sessionMessages));
|
|
156
|
+
touched.push(runtime);
|
|
157
|
+
try {
|
|
158
|
+
await client.heartbeatSession(session.id, {
|
|
159
|
+
state: runtime.state,
|
|
160
|
+
sessionRef: runtime.sessionRef,
|
|
161
|
+
lastError: runtime.state === "stale" ? runtime.sessionRef?.reason || "host_unavailable" : null,
|
|
162
|
+
idempotencyKey: idempotencyKey("heartbeat"),
|
|
163
|
+
});
|
|
164
|
+
await client.postDaemonEvent(session.taskId, {
|
|
165
|
+
sessionId: session.id,
|
|
166
|
+
type: "daemon.heartbeat",
|
|
167
|
+
body: `Relay companion heartbeat for ${runtime.host}.`,
|
|
168
|
+
payload: { runtimeState: runtime.state, sessionRef: runtime.sessionRef },
|
|
169
|
+
idempotencyKey: idempotencyKey("daemon_event"),
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
log(`heartbeat failed for ${session.id}: ${err.message}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (processedMessages.length) {
|
|
177
|
+
for (const message of processedMessages) {
|
|
178
|
+
log(`received task message ${message.id} (${message.kind}) for task ${message.taskId}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const humanPolling = await pollHumanNotifications({ client, ledger, stageCompanionItem, log });
|
|
183
|
+
writeTaskLedger(ledger);
|
|
184
|
+
return { sessions: touched, messages: processedMessages, notifications: humanPolling.notifications, events: humanPolling.events };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function runTaskDaemon({ intervalMs = 4000 } = {}) {
|
|
188
|
+
const client = new RelayClient();
|
|
189
|
+
const me = await client.me();
|
|
190
|
+
// eslint-disable-next-line no-console
|
|
191
|
+
console.log(`[relay] task runtime for ${me.user.email}; polling every ${intervalMs}ms`);
|
|
192
|
+
// eslint-disable-next-line no-console
|
|
193
|
+
const log = (m) => console.log(`[relay] ${m}`);
|
|
194
|
+
for (;;) {
|
|
195
|
+
try {
|
|
196
|
+
const result = await pollTaskRuntimeOnce({ client, log });
|
|
197
|
+
if (result.sessions.length || result.messages.length || result.notifications.length || result.events.length) {
|
|
198
|
+
log(
|
|
199
|
+
`processed ${result.sessions.length} session(s), ${result.messages.length} message(s), ${result.notifications.length} Relay companion item(s), ${result.events.length} event(s)`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
// eslint-disable-next-line no-console
|
|
204
|
+
console.error(`[relay] task daemon error: ${err.message}`);
|
|
205
|
+
}
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
211
|
+
runTaskDaemon().catch((err) => {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.error(err);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
216
|
+
}
|