instar 0.9.0 → 0.9.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/cli.js +0 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +202 -71
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +38 -4
- package/dist/commands/setup.js.map +1 -1
- package/dist/core/AgentConnector.d.ts +76 -0
- package/dist/core/AgentConnector.d.ts.map +1 -0
- package/dist/core/AgentConnector.js +323 -0
- package/dist/core/AgentConnector.js.map +1 -0
- package/dist/core/AutoUpdater.d.ts +7 -0
- package/dist/core/AutoUpdater.d.ts.map +1 -1
- package/dist/core/AutoUpdater.js +31 -3
- package/dist/core/AutoUpdater.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +86 -5
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/StateWriteAuthority.d.ts +101 -0
- package/dist/core/StateWriteAuthority.d.ts.map +1 -0
- package/dist/core/StateWriteAuthority.js +167 -0
- package/dist/core/StateWriteAuthority.js.map +1 -0
- package/dist/core/types.d.ts +104 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/TopicMemory.d.ts +167 -0
- package/dist/memory/TopicMemory.d.ts.map +1 -0
- package/dist/memory/TopicMemory.js +494 -0
- package/dist/memory/TopicMemory.js.map +1 -0
- package/dist/memory/TopicSummarizer.d.ts +58 -0
- package/dist/memory/TopicSummarizer.d.ts.map +1 -0
- package/dist/memory/TopicSummarizer.js +140 -0
- package/dist/memory/TopicSummarizer.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts +35 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +136 -2
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +340 -1
- package/dist/server/routes.js.map +1 -1
- package/dist/users/UserManager.d.ts +21 -0
- package/dist/users/UserManager.d.ts.map +1 -1
- package/dist/users/UserManager.js +32 -0
- package/dist/users/UserManager.js.map +1 -1
- package/dist/users/UserOnboarding.d.ts +116 -0
- package/dist/users/UserOnboarding.d.ts.map +1 -0
- package/dist/users/UserOnboarding.js +365 -0
- package/dist/users/UserOnboarding.js.map +1 -0
- package/package.json +2 -1
- package/upgrades/0.8.23.md +106 -0
- package/upgrades/0.9.1.md +91 -0
package/dist/server/routes.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { rateLimiter, signViewPath } from './middleware.js';
|
|
14
|
+
import { validateWriteToken, canPerformOperation } from '../core/StateWriteAuthority.js';
|
|
14
15
|
// Validation patterns for route parameters
|
|
15
16
|
const SESSION_NAME_RE = /^[a-zA-Z0-9_-]{1,200}$/;
|
|
16
17
|
const JOB_SLUG_RE = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
@@ -431,6 +432,18 @@ export function createRoutes(ctx) {
|
|
|
431
432
|
users: {
|
|
432
433
|
count: userCount,
|
|
433
434
|
},
|
|
435
|
+
topicMemory: {
|
|
436
|
+
enabled: !!ctx.topicMemory,
|
|
437
|
+
stats: ctx.topicMemory?.stats() ?? null,
|
|
438
|
+
endpoints: ctx.topicMemory ? [
|
|
439
|
+
'GET /topic/search?q=...&topic=N&limit=20',
|
|
440
|
+
'GET /topic/context/:topicId?recent=30',
|
|
441
|
+
'GET /topic/list',
|
|
442
|
+
'GET /topic/stats',
|
|
443
|
+
'POST /topic/summarize { topicId }',
|
|
444
|
+
'POST /topic/summary { topicId, summary, messageCount, lastMessageId }',
|
|
445
|
+
] : [],
|
|
446
|
+
},
|
|
434
447
|
monitoring: ctx.config.monitoring,
|
|
435
448
|
evolution: {
|
|
436
449
|
enabled: !!ctx.evolution,
|
|
@@ -460,7 +473,7 @@ export function createRoutes(ctx) {
|
|
|
460
473
|
{ context: 'User has a recurring task', action: 'Create a scheduled job in .instar/jobs.json. Explain it will run automatically.' },
|
|
461
474
|
{ context: 'User repeats a workflow', action: 'Create a skill in .claude/skills/. It becomes a slash command for future sessions.' },
|
|
462
475
|
{ context: 'User is debugging CI or deployment', action: 'Check CI health (GET /ci) for GitHub Actions status.' },
|
|
463
|
-
{ context: 'User asks about past events', action: 'Search
|
|
476
|
+
{ context: 'User asks about past events or prior conversations', action: 'Search topic memory (GET /topic/search?q=...), get topic context (GET /topic/context/:topicId), check memory, review activity logs.' },
|
|
464
477
|
{ context: 'User frustrated with a limitation', action: 'Check for updates (GET /updates). Check dispatches (GET /dispatches/pending). The fix may already exist.' },
|
|
465
478
|
{ context: 'User asks to remember something', action: 'Write to .instar/MEMORY.md. Explain it persists across sessions.' },
|
|
466
479
|
{ context: 'Something needs user attention later', action: 'Queue in attention system (POST /attention). More reliable than hoping they see a message.' },
|
|
@@ -1947,6 +1960,332 @@ export function createRoutes(ctx) {
|
|
|
1947
1960
|
ctx.watchdog.setEnabled(enabled);
|
|
1948
1961
|
res.json({ enabled: ctx.watchdog.isEnabled() });
|
|
1949
1962
|
});
|
|
1963
|
+
// ── Topic Memory (conversation search & context) ─────────────────────
|
|
1964
|
+
/**
|
|
1965
|
+
* Search topic message history with FTS5 full-text search.
|
|
1966
|
+
* GET /topic/search?q=query&topic=topicId&limit=20
|
|
1967
|
+
*/
|
|
1968
|
+
router.get('/topic/search', (req, res) => {
|
|
1969
|
+
if (!ctx.topicMemory) {
|
|
1970
|
+
res.status(503).json({ error: 'TopicMemory not initialized' });
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const q = (req.query.q || '').trim();
|
|
1974
|
+
if (!q) {
|
|
1975
|
+
res.status(400).json({ error: 'q (search query) required' });
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
const topicId = req.query.topic ? parseInt(req.query.topic, 10) : undefined;
|
|
1979
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
1980
|
+
const results = ctx.topicMemory.search(q, { topicId, limit });
|
|
1981
|
+
res.json({ query: q, topicId: topicId ?? null, results, totalResults: results.length });
|
|
1982
|
+
});
|
|
1983
|
+
/**
|
|
1984
|
+
* Get full context for a topic (summary + recent messages).
|
|
1985
|
+
* GET /topic/context/:topicId?recent=30
|
|
1986
|
+
*/
|
|
1987
|
+
router.get('/topic/context/:topicId', (req, res) => {
|
|
1988
|
+
if (!ctx.topicMemory) {
|
|
1989
|
+
res.status(503).json({ error: 'TopicMemory not initialized' });
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const topicId = parseInt(req.params.topicId, 10);
|
|
1993
|
+
if (isNaN(topicId)) {
|
|
1994
|
+
res.status(400).json({ error: 'Invalid topicId' });
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const recentLimit = Math.min(parseInt(req.query.recent, 10) || 30, 100);
|
|
1998
|
+
const context = ctx.topicMemory.getTopicContext(topicId, recentLimit);
|
|
1999
|
+
res.json(context);
|
|
2000
|
+
});
|
|
2001
|
+
/**
|
|
2002
|
+
* List all topics with metadata.
|
|
2003
|
+
* GET /topic/list
|
|
2004
|
+
*/
|
|
2005
|
+
router.get('/topic/list', (_req, res) => {
|
|
2006
|
+
if (!ctx.topicMemory) {
|
|
2007
|
+
res.status(503).json({ error: 'TopicMemory not initialized' });
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
const topics = ctx.topicMemory.listTopics();
|
|
2011
|
+
res.json({ topics, total: topics.length });
|
|
2012
|
+
});
|
|
2013
|
+
/**
|
|
2014
|
+
* Get topic memory stats.
|
|
2015
|
+
* GET /topic/stats
|
|
2016
|
+
*/
|
|
2017
|
+
router.get('/topic/stats', (_req, res) => {
|
|
2018
|
+
if (!ctx.topicMemory) {
|
|
2019
|
+
res.status(503).json({ error: 'TopicMemory not initialized' });
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
res.json(ctx.topicMemory.stats());
|
|
2023
|
+
});
|
|
2024
|
+
/**
|
|
2025
|
+
* Trigger summary generation for a topic.
|
|
2026
|
+
* POST /topic/summarize { topicId: number }
|
|
2027
|
+
*/
|
|
2028
|
+
router.post('/topic/summarize', (req, res) => {
|
|
2029
|
+
if (!ctx.topicMemory) {
|
|
2030
|
+
res.status(503).json({ error: 'TopicMemory not initialized' });
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
const topicId = req.body?.topicId;
|
|
2034
|
+
if (typeof topicId !== 'number') {
|
|
2035
|
+
res.status(400).json({ error: 'topicId (number) required' });
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const needsUpdate = ctx.topicMemory.needsSummaryUpdate(topicId, 1);
|
|
2039
|
+
const messagesSince = ctx.topicMemory.getMessagesSinceSummary(topicId);
|
|
2040
|
+
const currentSummary = ctx.topicMemory.getTopicSummary(topicId);
|
|
2041
|
+
// Return the data needed for an LLM to generate the summary.
|
|
2042
|
+
// The actual LLM call happens in the calling session (not in the HTTP handler).
|
|
2043
|
+
res.json({
|
|
2044
|
+
topicId,
|
|
2045
|
+
needsUpdate,
|
|
2046
|
+
currentSummary: currentSummary?.summary ?? null,
|
|
2047
|
+
messagesSinceSummary: messagesSince.length,
|
|
2048
|
+
messages: messagesSince.map(m => ({
|
|
2049
|
+
from: m.fromUser ? 'User' : 'Agent',
|
|
2050
|
+
text: m.text,
|
|
2051
|
+
timestamp: m.timestamp,
|
|
2052
|
+
messageId: m.messageId,
|
|
2053
|
+
})),
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
/**
|
|
2057
|
+
* Save a generated summary for a topic.
|
|
2058
|
+
* POST /topic/summary { topicId, summary, messageCount, lastMessageId }
|
|
2059
|
+
*/
|
|
2060
|
+
router.post('/topic/summary', (req, res) => {
|
|
2061
|
+
if (!ctx.topicMemory) {
|
|
2062
|
+
res.status(503).json({ error: 'TopicMemory not initialized' });
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
const { topicId, summary, messageCount, lastMessageId } = req.body || {};
|
|
2066
|
+
if (typeof topicId !== 'number' || typeof summary !== 'string') {
|
|
2067
|
+
res.status(400).json({ error: 'topicId (number) and summary (string) required' });
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
ctx.topicMemory.saveTopicSummary(topicId, summary, messageCount ?? 0, lastMessageId ?? 0);
|
|
2071
|
+
res.json({ saved: true, topicId });
|
|
2072
|
+
});
|
|
2073
|
+
/**
|
|
2074
|
+
* Rebuild topic memory from JSONL (idempotent import).
|
|
2075
|
+
* POST /topic/rebuild
|
|
2076
|
+
*/
|
|
2077
|
+
router.post('/topic/rebuild', (_req, res) => {
|
|
2078
|
+
if (!ctx.topicMemory) {
|
|
2079
|
+
res.status(503).json({ error: 'TopicMemory not initialized' });
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const jsonlPath = path.join(ctx.config.stateDir, 'telegram-messages.jsonl');
|
|
2083
|
+
const imported = ctx.topicMemory.rebuild(jsonlPath);
|
|
2084
|
+
res.json({ rebuilt: true, messagesImported: imported, stats: ctx.topicMemory.stats() });
|
|
2085
|
+
});
|
|
2086
|
+
// ── Pairing API — Multi-machine state sync (Phase 4.5) ────────
|
|
2087
|
+
/**
|
|
2088
|
+
* POST /state/submit — Secondary machine submits a state change.
|
|
2089
|
+
* Validates write token, checks operation authorization, applies or queues.
|
|
2090
|
+
*/
|
|
2091
|
+
router.post('/state/submit', (req, res) => {
|
|
2092
|
+
const { operation, payload, machineId, writeToken } = req.body || {};
|
|
2093
|
+
// Validate required fields
|
|
2094
|
+
if (!operation || !payload || !machineId || !writeToken) {
|
|
2095
|
+
res.status(400).json({
|
|
2096
|
+
error: 'Missing required fields: operation, payload, machineId, writeToken',
|
|
2097
|
+
});
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (typeof operation !== 'string' || typeof machineId !== 'string' || typeof writeToken !== 'string') {
|
|
2101
|
+
res.status(400).json({ error: 'operation, machineId, and writeToken must be strings' });
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
// Load stored write tokens
|
|
2105
|
+
const tokensFile = path.join(ctx.config.stateDir, 'write-tokens.json');
|
|
2106
|
+
let storedTokens = [];
|
|
2107
|
+
try {
|
|
2108
|
+
if (fs.existsSync(tokensFile)) {
|
|
2109
|
+
storedTokens = JSON.parse(fs.readFileSync(tokensFile, 'utf-8'));
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
catch {
|
|
2113
|
+
res.status(500).json({ error: 'Failed to load write tokens' });
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
// Validate the write token
|
|
2117
|
+
const tokenResult = validateWriteToken(writeToken, storedTokens);
|
|
2118
|
+
if (!tokenResult.valid) {
|
|
2119
|
+
res.status(403).json({ error: tokenResult.error });
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
// Verify the token was issued to the claiming machine
|
|
2123
|
+
if (tokenResult.machineId !== machineId) {
|
|
2124
|
+
res.status(403).json({ error: 'Write token does not match machineId' });
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
// Check if the operation is allowed
|
|
2128
|
+
const opCheck = canPerformOperation(operation);
|
|
2129
|
+
if (!opCheck.allowed) {
|
|
2130
|
+
res.status(403).json({
|
|
2131
|
+
error: opCheck.reason,
|
|
2132
|
+
requiresConfirmation: opCheck.requiresConfirmation,
|
|
2133
|
+
});
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
// Apply the state change based on operation type
|
|
2137
|
+
try {
|
|
2138
|
+
switch (operation) {
|
|
2139
|
+
case 'addMemory': {
|
|
2140
|
+
// Append memory entry to memories.jsonl
|
|
2141
|
+
const memoriesFile = path.join(ctx.config.stateDir, 'memories.jsonl');
|
|
2142
|
+
const entry = { ...payload, sourceMachineId: machineId, appliedAt: new Date().toISOString() };
|
|
2143
|
+
fs.appendFileSync(memoriesFile, JSON.stringify(entry) + '\n');
|
|
2144
|
+
res.json({ applied: true, operation });
|
|
2145
|
+
break;
|
|
2146
|
+
}
|
|
2147
|
+
case 'updateProfile': {
|
|
2148
|
+
// Update a user profile field
|
|
2149
|
+
const usersFile = path.join(ctx.config.stateDir, 'users.json');
|
|
2150
|
+
if (!fs.existsSync(usersFile)) {
|
|
2151
|
+
res.status(404).json({ error: 'No users file found' });
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const users = JSON.parse(fs.readFileSync(usersFile, 'utf-8'));
|
|
2155
|
+
const targetUser = users.find((u) => u.id === payload.userId);
|
|
2156
|
+
if (!targetUser) {
|
|
2157
|
+
res.status(404).json({ error: `User ${payload.userId} not found` });
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
// Apply the update fields (shallow merge)
|
|
2161
|
+
if (payload.updates && typeof payload.updates === 'object') {
|
|
2162
|
+
Object.assign(targetUser, payload.updates);
|
|
2163
|
+
}
|
|
2164
|
+
fs.writeFileSync(usersFile, JSON.stringify(users, null, 2));
|
|
2165
|
+
res.json({ applied: true, operation, userId: payload.userId });
|
|
2166
|
+
break;
|
|
2167
|
+
}
|
|
2168
|
+
case 'heartbeat': {
|
|
2169
|
+
// Heartbeat is handled by the dedicated endpoint below
|
|
2170
|
+
res.json({ applied: true, operation });
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
default: {
|
|
2174
|
+
res.status(400).json({ error: `Unknown operation: ${operation}` });
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
catch (err) {
|
|
2179
|
+
res.status(500).json({
|
|
2180
|
+
error: 'Failed to apply state change',
|
|
2181
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
/**
|
|
2186
|
+
* GET /state/sync — Secondary machine pulls latest state.
|
|
2187
|
+
* Returns current users, config summary, and machine registry.
|
|
2188
|
+
*/
|
|
2189
|
+
router.get('/state/sync', (_req, res) => {
|
|
2190
|
+
try {
|
|
2191
|
+
// Read users
|
|
2192
|
+
const usersFile = path.join(ctx.config.stateDir, 'users.json');
|
|
2193
|
+
let users = [];
|
|
2194
|
+
if (fs.existsSync(usersFile)) {
|
|
2195
|
+
try {
|
|
2196
|
+
users = JSON.parse(fs.readFileSync(usersFile, 'utf-8'));
|
|
2197
|
+
}
|
|
2198
|
+
catch { /* empty array on corruption */ }
|
|
2199
|
+
}
|
|
2200
|
+
// Read machine registry
|
|
2201
|
+
const registryFile = path.join(ctx.config.stateDir, 'machine-registry.json');
|
|
2202
|
+
let machineRegistry = { version: 1, machines: {} };
|
|
2203
|
+
if (fs.existsSync(registryFile)) {
|
|
2204
|
+
try {
|
|
2205
|
+
machineRegistry = JSON.parse(fs.readFileSync(registryFile, 'utf-8'));
|
|
2206
|
+
}
|
|
2207
|
+
catch { /* default on corruption */ }
|
|
2208
|
+
}
|
|
2209
|
+
// Config summary (non-sensitive fields only)
|
|
2210
|
+
const configSummary = {
|
|
2211
|
+
projectName: ctx.config.projectName,
|
|
2212
|
+
userRegistrationPolicy: ctx.config.userRegistrationPolicy ?? 'admin-only',
|
|
2213
|
+
agentAutonomy: ctx.config.agentAutonomy?.level ?? 'supervised',
|
|
2214
|
+
multiMachine: ctx.config.multiMachine ?? { enabled: false },
|
|
2215
|
+
userCount: users.length,
|
|
2216
|
+
};
|
|
2217
|
+
res.json({
|
|
2218
|
+
users,
|
|
2219
|
+
machineRegistry,
|
|
2220
|
+
configSummary,
|
|
2221
|
+
syncedAt: new Date().toISOString(),
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
catch (err) {
|
|
2225
|
+
res.status(500).json({
|
|
2226
|
+
error: 'Failed to sync state',
|
|
2227
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
/**
|
|
2232
|
+
* POST /state/heartbeat — Secondary machine reports online status.
|
|
2233
|
+
* Updates lastSeen for the machine and returns queued change count.
|
|
2234
|
+
*/
|
|
2235
|
+
router.post('/state/heartbeat', (req, res) => {
|
|
2236
|
+
const { machineId } = req.body || {};
|
|
2237
|
+
if (!machineId || typeof machineId !== 'string') {
|
|
2238
|
+
res.status(400).json({ error: 'machineId (string) required' });
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
try {
|
|
2242
|
+
// Update machine lastSeen in registry
|
|
2243
|
+
const registryFile = path.join(ctx.config.stateDir, 'machine-registry.json');
|
|
2244
|
+
let registry = {
|
|
2245
|
+
version: 1,
|
|
2246
|
+
machines: {},
|
|
2247
|
+
};
|
|
2248
|
+
if (fs.existsSync(registryFile)) {
|
|
2249
|
+
try {
|
|
2250
|
+
registry = JSON.parse(fs.readFileSync(registryFile, 'utf-8'));
|
|
2251
|
+
}
|
|
2252
|
+
catch { /* use default */ }
|
|
2253
|
+
}
|
|
2254
|
+
if (registry.machines[machineId]) {
|
|
2255
|
+
registry.machines[machineId].lastSeen = new Date().toISOString();
|
|
2256
|
+
fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
|
|
2257
|
+
}
|
|
2258
|
+
// Count queued changes for this machine (from offline queue if it exists)
|
|
2259
|
+
const queueFile = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.instar', 'offline-queue', `${ctx.config.projectName}.jsonl`);
|
|
2260
|
+
let queuedChanges = 0;
|
|
2261
|
+
if (fs.existsSync(queueFile)) {
|
|
2262
|
+
const content = fs.readFileSync(queueFile, 'utf-8').trim();
|
|
2263
|
+
if (content) {
|
|
2264
|
+
queuedChanges = content.split('\n').filter(line => {
|
|
2265
|
+
try {
|
|
2266
|
+
const entry = JSON.parse(line);
|
|
2267
|
+
return entry.sourceMachineId === machineId;
|
|
2268
|
+
}
|
|
2269
|
+
catch {
|
|
2270
|
+
return false;
|
|
2271
|
+
}
|
|
2272
|
+
}).length;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
res.json({
|
|
2276
|
+
status: 'ok',
|
|
2277
|
+
machineId,
|
|
2278
|
+
queuedChanges,
|
|
2279
|
+
timestamp: new Date().toISOString(),
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
catch (err) {
|
|
2283
|
+
res.status(500).json({
|
|
2284
|
+
error: 'Heartbeat processing failed',
|
|
2285
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
1950
2289
|
return router;
|
|
1951
2290
|
}
|
|
1952
2291
|
export function formatUptime(ms) {
|