@livedesk/hub 0.1.43 → 0.1.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -0,0 +1,66 @@
1
+ const DEFAULT_MAX_STATUS_AGE_MS = 30_000;
2
+
3
+ function asRecord(value) {
4
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
5
+ }
6
+
7
+ function cpuUsageRatio(value) {
8
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1
9
+ ? value
10
+ : null;
11
+ }
12
+
13
+ function positiveInteger(value) {
14
+ const number = Number(value);
15
+ return Number.isSafeInteger(number) && number > 0 ? number : null;
16
+ }
17
+
18
+ function statusAgeMs(device, now) {
19
+ const sampledAt = Date.parse(String(device?.lastStatusAt || ''));
20
+ return Number.isFinite(sampledAt) ? Math.max(0, now - sampledAt) : null;
21
+ }
22
+
23
+ export function enrichAgentTaskResults({
24
+ operation,
25
+ results,
26
+ devices,
27
+ now = Date.now(),
28
+ maxStatusAgeMs = DEFAULT_MAX_STATUS_AGE_MS
29
+ } = {}) {
30
+ const sourceResults = Array.isArray(results) ? results : [];
31
+ if (operation !== 'system.health') return sourceResults;
32
+
33
+ const devicesById = new Map(
34
+ (Array.isArray(devices) ? devices : []).map(device => [String(device?.deviceId || ''), device])
35
+ );
36
+
37
+ return sourceResults.map(result => {
38
+ if (!result || result.status !== 'completed') return result;
39
+ const data = asRecord(result.data);
40
+ const device = devicesById.get(String(result.deviceId || ''));
41
+ const status = asRecord(device?.status);
42
+ const statusCpu = asRecord(status.cpu);
43
+ const directRatio = cpuUsageRatio(data.cpuUsageRatio);
44
+ const latestRatio = cpuUsageRatio(statusCpu.usageRatio);
45
+ const latestAgeMs = statusAgeMs(device, now);
46
+ const useLatestStatus = directRatio === null
47
+ && latestRatio !== null
48
+ && latestAgeMs !== null
49
+ && latestAgeMs <= Math.max(0, Number(maxStatusAgeMs) || DEFAULT_MAX_STATUS_AGE_MS);
50
+ const cores = positiveInteger(data.cores) || positiveInteger(statusCpu.cores);
51
+ const nextData = { ...data };
52
+
53
+ if (cores !== null) nextData.cores = cores;
54
+ if (directRatio !== null) {
55
+ nextData.cpuUsageRatio = directRatio;
56
+ nextData.cpuUsageSource = 'system-health-task';
57
+ } else if (useLatestStatus) {
58
+ nextData.cpuUsageRatio = latestRatio;
59
+ nextData.cpuUsageSource = 'latest-client-status';
60
+ nextData.cpuUsageSampleAgeMs = latestAgeMs;
61
+ nextData.cpuUsageSampledAt = String(device.lastStatusAt);
62
+ }
63
+
64
+ return { ...result, data: nextData };
65
+ });
66
+ }
@@ -728,8 +728,10 @@ export function createCodexAgentRuntime({
728
728
  'Never use arbitrary MCP servers, change permissions, forge approvals, request credentials, or invent a tool result.',
729
729
  `The Hub has fixed this run to permission mode ${permissionPolicy?.mode || 'ask'} and enforces the policy independently of your instructions.`,
730
730
  'Use only the selected connected device IDs below. If the Hub asks for user approval, wait for that approval result and do not work around it.',
731
- 'You may perform multiple safe read-only checks when the request requires a sequence. For example, find Clients missing a named process, then check a related service only on those Clients.',
732
- `Selected device IDs: ${JSON.stringify(deviceIds)}`,
731
+ 'You may perform multiple safe read-only checks when the request requires a sequence. For example, find Clients missing a named process, then check a related service only on those Clients.',
732
+ 'If one read-only result omits a fact the user requested, do not treat that missing field as proof that the fact is unavailable. Try another applicable registered LiveDesk read-only tool.',
733
+ 'If only a high-risk registered tool such as livedesk.run_command can obtain the missing fact, use it only when necessary, one selected device per call, and let the Hub approval policy ask the user. If approval is denied or no applicable tool exists, explain that exact limit.',
734
+ `Selected device IDs: ${JSON.stringify(deviceIds)}`,
733
735
  'Return a concise Korean or English summary grounded only in tool results. Do not invent results.',
734
736
  `User request: ${safeText(instruction, 4000)}`
735
737
  ].join('\n');
@@ -107,9 +107,10 @@ export function createHubConsoleRelay(options = {}) {
107
107
  let lastError = relayUrl instanceof URL ? '' : relayUrl === '' ? '' : 'invalid-relay-url';
108
108
  let droppedBinaryMessages = 0;
109
109
 
110
- const sendRelay = payload => {
110
+ const sendRelay = (payload, bypassBackpressure = false) => {
111
111
  if (!relaySocket || relaySocket.readyState !== WebSocketImpl.OPEN) return false;
112
- if (Number(relaySocket.bufferedAmount || 0) > MAX_RELAY_BUFFERED_BYTES) return false;
112
+ if (!bypassBackpressure
113
+ && Number(relaySocket.bufferedAmount || 0) > MAX_RELAY_BUFFERED_BYTES) return false;
113
114
  relaySocket.send(typeof payload === 'string' || Buffer.isBuffer(payload) ? payload : JSON.stringify(payload));
114
115
  return true;
115
116
  };
@@ -232,6 +233,7 @@ export function createHubConsoleRelay(options = {}) {
232
233
  if (!relaySocket || relaySocket.readyState !== WebSocketImpl.OPEN
233
234
  || Number(relaySocket.bufferedAmount || 0) > MAX_RELAY_BUFFERED_BYTES) {
234
235
  droppedBinaryMessages += 1;
236
+ closeLocalSocket(key, 1013, 'console-relay-backpressure');
235
237
  return;
236
238
  }
237
239
  const header = Buffer.from(`B${consoleId}${channelId}`, 'ascii');
@@ -246,7 +248,14 @@ export function createHubConsoleRelay(options = {}) {
246
248
  });
247
249
  socket.once('close', (code, reason) => {
248
250
  if (localSockets.get(key) === socket) localSockets.delete(key);
249
- sendRelay({ type: 'ws-closed', consoleId, channelId, code, reason: String(reason || '').slice(0, 120) });
251
+ const closeReason = String(reason || '').slice(0, 120);
252
+ // Do not silently punch a hole in an H.264 GOP. A tiny terminal control
253
+ // message is allowed behind the already-bounded video backlog so the
254
+ // browser replaces only this logical lane and starts again on a key.
255
+ sendRelay(
256
+ { type: 'ws-closed', consoleId, channelId, code, reason: closeReason },
257
+ closeReason === 'console-relay-backpressure'
258
+ );
250
259
  });
251
260
  socket.once('error', error => {
252
261
  sendError(consoleId, channelId, error instanceof Error ? error.message : error, 'ws-error');
package/src/server.js CHANGED
@@ -40,8 +40,9 @@ import { createAgentDeviceScope, resolveAgentTargetIds } from './agents/agent-de
40
40
  import { AgentRuntimeError } from './agents/agent-runtime-error.js';
41
41
  import { AGENT_PERMISSION_MODES, createAgentPermissionPolicy, evaluateAgentToolPermission, hashAgentPermissionPolicy } from './agents/agent-permissions.js';
42
42
  import { createAgentPermissionStore } from './agents/agent-permission-store.js';
43
- import { createAgentAuditStore } from './agents/agent-audit-store.js';
44
- import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
43
+ import { createAgentAuditStore } from './agents/agent-audit-store.js';
44
+ import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
45
+ import { enrichAgentTaskResults } from './agents/agent-result-enrichment.js';
45
46
  import { LiveDeskSettingsStore, SettingsConflictError } from './settings/settings-store.js';
46
47
  import { effectiveDevicePolicy } from './settings/settings-schema.js';
47
48
  import { buildEffectiveDevicePolicy } from './settings/effective-device-policy.js';
@@ -1269,10 +1270,16 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
1269
1270
  remoteHub.cancelTaskBatch(result.batchId);
1270
1271
  return { ok: false, error: 'cancelled-by-user', batchId: result.batchId };
1271
1272
  }
1272
- const batch = remoteHub.getTaskBatch(result.batchId);
1273
- if (!batch) return { ok: false, error: 'task-not-found', batchId: result.batchId };
1274
- if (!['queued', 'running'].includes(batch.status)) {
1275
- return {
1273
+ const batch = remoteHub.getTaskBatch(result.batchId);
1274
+ if (!batch) return { ok: false, error: 'task-not-found', batchId: result.batchId };
1275
+ if (!['queued', 'running'].includes(batch.status)) {
1276
+ const taskResults = enrichAgentTaskResults({
1277
+ operation,
1278
+ results: batch.results,
1279
+ devices: remoteHub.listDevices({ includeDataUrl: false })
1280
+ .filter(device => targetIds.includes(device.deviceId))
1281
+ });
1282
+ return {
1276
1283
  ok: true,
1277
1284
  batchId: result.batchId,
1278
1285
  operation,
@@ -1280,8 +1287,8 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
1280
1287
  total: batch.total,
1281
1288
  completed: batch.completed,
1282
1289
  failed: batch.failed,
1283
- results: batch.results.map(item => ({ deviceId: item.deviceId, deviceName: item.deviceName, status: item.status, stage: item.stage, result: String(item.result || '').slice(0, 3000), data: item.data, error: String(item.error || '').slice(0, 500) }))
1284
- };
1290
+ results: taskResults.map(item => ({ deviceId: item.deviceId, deviceName: item.deviceName, status: item.status, stage: item.stage, result: String(item.result || '').slice(0, 3000), data: item.data, error: String(item.error || '').slice(0, 500) }))
1291
+ };
1285
1292
  }
1286
1293
  await delayAgentMcp(200);
1287
1294
  }