agent-state-machine 2.0.4 → 2.0.5
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/lib/remote/client.js +12 -0
- package/lib/runtime/runtime.js +50 -0
- package/package.json +1 -1
- package/vercel-server/api/events/[token].js +21 -35
- package/vercel-server/api/ws/cli.js +23 -30
package/lib/remote/client.js
CHANGED
|
@@ -164,6 +164,18 @@ export class RemoteClient {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Sync history to remote (for manual edits to history.jsonl)
|
|
169
|
+
* @param {Array} history - Array of history entries
|
|
170
|
+
*/
|
|
171
|
+
async sendHistorySync(history = []) {
|
|
172
|
+
await this.send({
|
|
173
|
+
type: 'history_sync',
|
|
174
|
+
sessionToken: this.sessionToken,
|
|
175
|
+
history,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
167
179
|
/**
|
|
168
180
|
* Send an event to the server
|
|
169
181
|
* @param {object} event - Event object with timestamp, event type, etc.
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -164,6 +164,9 @@ export class WorkflowRuntime {
|
|
|
164
164
|
|
|
165
165
|
const line = JSON.stringify(entry) + '\n';
|
|
166
166
|
|
|
167
|
+
// Track when we're writing to avoid triggering the file watcher
|
|
168
|
+
this._lastHistoryWrite = Date.now();
|
|
169
|
+
|
|
167
170
|
// Prepend to file (read existing, write new + existing)
|
|
168
171
|
let existing = '';
|
|
169
172
|
if (fs.existsSync(this.historyFile)) {
|
|
@@ -539,16 +542,63 @@ export class WorkflowRuntime {
|
|
|
539
542
|
this.remoteEnabled = true;
|
|
540
543
|
this.remoteUrl = this.remoteClient.getRemoteUrl();
|
|
541
544
|
|
|
545
|
+
// Watch history.jsonl for manual edits and sync to remote
|
|
546
|
+
this.startHistoryWatcher();
|
|
547
|
+
|
|
542
548
|
console.log(`\n${C.cyan}${C.bold}Remote follow enabled${C.reset}`);
|
|
543
549
|
console.log(` ${C.dim}URL:${C.reset} ${this.remoteUrl}\n`);
|
|
544
550
|
|
|
545
551
|
return this.remoteUrl;
|
|
546
552
|
}
|
|
547
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Start watching history.jsonl for manual edits
|
|
556
|
+
*/
|
|
557
|
+
startHistoryWatcher() {
|
|
558
|
+
if (this.historyWatcher) return;
|
|
559
|
+
|
|
560
|
+
// Debounce to avoid multiple syncs for rapid changes
|
|
561
|
+
let debounceTimer = null;
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
this.historyWatcher = fs.watch(this.historyFile, (eventType) => {
|
|
565
|
+
if (eventType !== 'change') return;
|
|
566
|
+
|
|
567
|
+
// Debounce: wait 300ms after last change before syncing
|
|
568
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
569
|
+
|
|
570
|
+
debounceTimer = setTimeout(async () => {
|
|
571
|
+
// Don't sync if we just wrote to the file ourselves (within 500ms)
|
|
572
|
+
if (this._lastHistoryWrite && Date.now() - this._lastHistoryWrite < 500) return;
|
|
573
|
+
|
|
574
|
+
if (this.remoteClient && this.remoteEnabled) {
|
|
575
|
+
const history = this.loadHistory();
|
|
576
|
+
await this.remoteClient.sendHistorySync(history);
|
|
577
|
+
console.log(`${C.dim}Remote: History synced from file${C.reset}`);
|
|
578
|
+
}
|
|
579
|
+
}, 300);
|
|
580
|
+
});
|
|
581
|
+
} catch (err) {
|
|
582
|
+
// File might not exist yet, that's ok
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Stop watching history.jsonl
|
|
588
|
+
*/
|
|
589
|
+
stopHistoryWatcher() {
|
|
590
|
+
if (this.historyWatcher) {
|
|
591
|
+
this.historyWatcher.close();
|
|
592
|
+
this.historyWatcher = null;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
548
596
|
/**
|
|
549
597
|
* Disable remote follow mode and disconnect
|
|
550
598
|
*/
|
|
551
599
|
async disableRemote() {
|
|
600
|
+
this.stopHistoryWatcher();
|
|
601
|
+
|
|
552
602
|
if (this.remoteClient) {
|
|
553
603
|
await this.remoteClient.disconnect();
|
|
554
604
|
this.remoteClient = null;
|
package/package.json
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* File: /vercel-server/api/events/[token].js
|
|
3
3
|
*
|
|
4
4
|
* SSE endpoint for browser connections
|
|
5
|
-
* Streams
|
|
5
|
+
* Streams events to connected browsers from a single source of truth (events list)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
getSession,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
getEvents,
|
|
11
|
+
getEventsLength,
|
|
12
|
+
getEventsRange,
|
|
13
13
|
refreshSession,
|
|
14
14
|
} from '../../lib/redis.js';
|
|
15
15
|
|
|
@@ -48,32 +48,15 @@ export default async function handler(req, res) {
|
|
|
48
48
|
workflowName: session.workflowName,
|
|
49
49
|
})}\n\n`);
|
|
50
50
|
|
|
51
|
-
// Send existing
|
|
52
|
-
const
|
|
51
|
+
// Send all existing events (single source of truth)
|
|
52
|
+
const events = await getEvents(token);
|
|
53
53
|
res.write(`data: ${JSON.stringify({
|
|
54
54
|
type: 'history',
|
|
55
|
-
entries:
|
|
55
|
+
entries: events,
|
|
56
56
|
})}\n\n`);
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
let lastEventIndex = 0;
|
|
61
|
-
|
|
62
|
-
// If history is empty, seed from the event list to catch early events.
|
|
63
|
-
const currentLength = await redis.llen(eventsListKey);
|
|
64
|
-
// if (history.length === 0 && currentLength > 0) {
|
|
65
|
-
// const existingEvents = await redis.lrange(eventsListKey, 0, -1);
|
|
66
|
-
// const entries = existingEvents
|
|
67
|
-
// .map((event) => (typeof event === 'object' ? event : JSON.parse(event)))
|
|
68
|
-
// .filter(Boolean);
|
|
69
|
-
// res.write(`data: ${JSON.stringify({
|
|
70
|
-
// type: 'history',
|
|
71
|
-
// entries,
|
|
72
|
-
// })}\n\n`);
|
|
73
|
-
// lastEventIndex = currentLength;
|
|
74
|
-
// } else {
|
|
75
|
-
// lastEventIndex = currentLength;
|
|
76
|
-
// }
|
|
58
|
+
// Track current position for polling new events
|
|
59
|
+
let lastEventIndex = await getEventsLength(token);
|
|
77
60
|
|
|
78
61
|
const pollInterval = setInterval(async () => {
|
|
79
62
|
try {
|
|
@@ -81,15 +64,19 @@ export default async function handler(req, res) {
|
|
|
81
64
|
await refreshSession(token);
|
|
82
65
|
|
|
83
66
|
// Check for new events
|
|
84
|
-
const newLength = await
|
|
67
|
+
const newLength = await getEventsLength(token);
|
|
85
68
|
|
|
86
69
|
if (newLength > lastEventIndex) {
|
|
87
|
-
// Get new events (newest
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
70
|
+
// Get new events (they're prepended, so newest are at the start)
|
|
71
|
+
const newCount = newLength - lastEventIndex;
|
|
72
|
+
const newEvents = await getEventsRange(token, 0, newCount - 1);
|
|
73
|
+
|
|
74
|
+
// Send in chronological order (oldest first of the new batch)
|
|
75
|
+
for (const eventData of newEvents.reverse()) {
|
|
76
|
+
res.write(`data: ${JSON.stringify({
|
|
77
|
+
type: 'event',
|
|
78
|
+
...eventData,
|
|
79
|
+
})}\n\n`);
|
|
93
80
|
}
|
|
94
81
|
|
|
95
82
|
lastEventIndex = newLength;
|
|
@@ -106,14 +93,13 @@ export default async function handler(req, res) {
|
|
|
106
93
|
} catch (err) {
|
|
107
94
|
console.error('Error polling events:', err);
|
|
108
95
|
}
|
|
109
|
-
},
|
|
96
|
+
}, 1000); // Poll every 1 second for faster updates
|
|
110
97
|
|
|
111
98
|
// Clean up on client disconnect
|
|
112
99
|
req.on('close', () => {
|
|
113
100
|
clearInterval(pollInterval);
|
|
114
101
|
});
|
|
115
102
|
|
|
116
|
-
// For Vercel, we need to keep the connection alive but also respect function timeout
|
|
117
103
|
// Send keepalive pings
|
|
118
104
|
const keepaliveInterval = setInterval(() => {
|
|
119
105
|
res.write(': keepalive\n\n');
|
|
@@ -12,9 +12,8 @@ import {
|
|
|
12
12
|
getSession,
|
|
13
13
|
updateSession,
|
|
14
14
|
setCLIConnected,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
publishEvent,
|
|
15
|
+
addEvent,
|
|
16
|
+
setEvents,
|
|
18
17
|
redis,
|
|
19
18
|
KEYS,
|
|
20
19
|
} from '../../lib/redis.js';
|
|
@@ -59,19 +58,8 @@ async function handlePost(req, res) {
|
|
|
59
58
|
// Create session
|
|
60
59
|
await createSession(sessionToken, { workflowName, cliConnected: true });
|
|
61
60
|
|
|
62
|
-
// Replace
|
|
63
|
-
await
|
|
64
|
-
|
|
65
|
-
// Store initial history
|
|
66
|
-
if (history && history.length > 0) {
|
|
67
|
-
await addHistoryEvents(sessionToken, history);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Notify browsers
|
|
71
|
-
await publishEvent(sessionToken, {
|
|
72
|
-
type: 'cli_connected',
|
|
73
|
-
workflowName,
|
|
74
|
-
});
|
|
61
|
+
// Replace events with the provided history snapshot (single source of truth)
|
|
62
|
+
await setEvents(sessionToken, history || []);
|
|
75
63
|
|
|
76
64
|
return res.status(200).json({ success: true });
|
|
77
65
|
}
|
|
@@ -82,9 +70,10 @@ async function handlePost(req, res) {
|
|
|
82
70
|
// Update session as connected
|
|
83
71
|
await setCLIConnected(sessionToken, true);
|
|
84
72
|
|
|
85
|
-
//
|
|
86
|
-
await
|
|
87
|
-
|
|
73
|
+
// Add reconnect event to events list
|
|
74
|
+
await addEvent(sessionToken, {
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
event: 'CLI_RECONNECTED',
|
|
88
77
|
workflowName,
|
|
89
78
|
});
|
|
90
79
|
|
|
@@ -104,14 +93,8 @@ async function handlePost(req, res) {
|
|
|
104
93
|
delete historyEvent.sessionToken;
|
|
105
94
|
delete historyEvent.type;
|
|
106
95
|
|
|
107
|
-
// Add to
|
|
108
|
-
await
|
|
109
|
-
|
|
110
|
-
// Notify browsers
|
|
111
|
-
await publishEvent(sessionToken, {
|
|
112
|
-
type: 'event',
|
|
113
|
-
...historyEvent,
|
|
114
|
-
});
|
|
96
|
+
// Add to events list (single source of truth)
|
|
97
|
+
await addEvent(sessionToken, historyEvent);
|
|
115
98
|
|
|
116
99
|
return res.status(200).json({ success: true });
|
|
117
100
|
}
|
|
@@ -122,15 +105,25 @@ async function handlePost(req, res) {
|
|
|
122
105
|
// Mark CLI as disconnected
|
|
123
106
|
await setCLIConnected(sessionToken, false);
|
|
124
107
|
|
|
125
|
-
//
|
|
126
|
-
await
|
|
127
|
-
|
|
108
|
+
// Add disconnect event to events list
|
|
109
|
+
await addEvent(sessionToken, {
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
event: 'CLI_DISCONNECTED',
|
|
128
112
|
reason,
|
|
129
113
|
});
|
|
130
114
|
|
|
131
115
|
return res.status(200).json({ success: true });
|
|
132
116
|
}
|
|
133
117
|
|
|
118
|
+
case 'history_sync': {
|
|
119
|
+
const { history } = body;
|
|
120
|
+
|
|
121
|
+
// Replace events with synced history (for manual edits to history.jsonl)
|
|
122
|
+
await setEvents(sessionToken, history || []);
|
|
123
|
+
|
|
124
|
+
return res.status(200).json({ success: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
134
127
|
default:
|
|
135
128
|
return res.status(400).json({ error: `Unknown message type: ${type}` });
|
|
136
129
|
}
|