ag-ai 0.4.0 → 0.4.1

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.
@@ -40,8 +40,6 @@ const runnerStack = (context) => {
40
40
  throw new Error('AG_AI_GENERATION_CIDR is required');
41
41
  const imageProvider = context.env.AG_AI_IMAGE_PROVIDER ?? 'openai';
42
42
  const opencodeProvider = context.env.AG_AI_OPENCODE_PROVIDER;
43
- // '1' makes the bridge claim cloud work when the queue pushes it (see runtime/push.mjs).
44
- const push = context.env.AG_AI_PUSH;
45
43
  const agAiPackage = context.env.AG_AI_PACKAGE;
46
44
  if (!agAiPackage)
47
45
  throw new Error('AG_AI_PACKAGE is required');
@@ -95,7 +93,6 @@ const runnerStack = (context) => {
95
93
  (0, common_1.env)('AG_AI_OPENCODE_URL', opencodeUrl),
96
94
  (0, common_1.env)('AG_AI_OPENCODE_PASSWORD_FILE', secrets.opencode.file),
97
95
  ...(opencodeProvider === undefined ? [] : [(0, common_1.env)('AG_AI_OPENCODE_PROVIDER', opencodeProvider)]),
98
- ...(push === undefined ? [] : [(0, common_1.env)('AG_AI_PUSH', push)]),
99
96
  (0, common_1.env)('AG_AI_IMAGE_PROVIDER', imageProvider),
100
97
  (0, common_1.env)('AG_AI_IMAGE_API_KEY_FILE', secrets.image.file),
101
98
  (0, common_1.env)('AG_AI_RUNNER_ID', common_1.RUNNER_APP),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ag-ai",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Provider-neutral AI client and cluster runtime for the rpi-mega cluster.",
5
5
  "license": "ISC",
6
6
  "author": "admin@gec.dev",
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
10
10
  import { createOpenCodeExecutor, DEFAULT_OPENCODE_URL } from './executor/opencode.mjs';
11
11
  import { createImageAdapter } from './image/adapter.mjs';
12
12
  import { normalizeGenerationRequest } from './protocol.mjs';
13
- import { createWorkSignal, parsePushFallbackMs, startPushChannel } from './push.mjs';
13
+ import { createWorkSignal, startPushChannel } from './push.mjs';
14
14
  import { createLocalRunner, startGenerationServer } from './server.mjs';
15
15
  import { GenerationStore } from './store.mjs';
16
16
  import { AnalyticaEvents } from './telemetry.mjs';
@@ -828,12 +828,6 @@ const readConfig = async (env = process.env) => {
828
828
  generationPort: env.AG_AI_GENERATION_PORT ?? DEFAULT_GENERATION_PORT,
829
829
  generationStore: path.resolve(env.AG_AI_GENERATION_STORE ?? DEFAULT_GENERATION_STORE),
830
830
  generationTokenFile: env.AG_AI_GENERATION_TOKEN_FILE ?? DEFAULT_GENERATION_TOKEN_FILE,
831
- // AG_AI_PUSH=1 claims cloud work when the queue pushes it instead of every few seconds.
832
- pushEnabled: env.AG_AI_PUSH === '1',
833
- pushTicketPath: env.AG_AI_PUSH_TICKET_PATH,
834
- pushHttpPath: env.AG_AI_PUSH_HTTP_PATH,
835
- // How long a healthy push channel goes without a safety-net claim.
836
- pushFallbackMs: parsePushFallbackMs(env.AG_AI_PUSH_FALLBACK_MS),
837
831
  analyticaToken,
838
832
  token,
839
833
  };
@@ -981,20 +975,14 @@ const main = async () => {
981
975
  process.once('SIGTERM', () => void stop('SIGTERM'));
982
976
  process.once('SIGINT', () => void stop('SIGINT'));
983
977
 
984
- const workSignal = createWorkSignal({
985
- enabled: config.pushEnabled,
986
- fallbackMs: config.pushFallbackMs,
978
+ // The queue pushes queued work over a WebSocket; cloud claims follow pushes.
979
+ const workSignal = createWorkSignal();
980
+ const push = startPushChannel({
981
+ baseUrl: new URL(config.queueUrl).origin,
982
+ token: config.token,
983
+ onWork: () => workSignal.wake(),
984
+ log: (message) => console.log(`[runner] ${redact(message, [config.token])}`),
987
985
  });
988
- const push = config.pushEnabled
989
- ? startPushChannel({
990
- baseUrl: new URL(config.queueUrl).origin,
991
- token: config.token,
992
- ticketPath: config.pushTicketPath,
993
- httpPath: config.pushHttpPath,
994
- onWork: () => workSignal.wake(),
995
- log: (message) => console.log(`[runner] ${redact(message, [config.token])}`),
996
- })
997
- : undefined;
998
986
  const heartbeatTimer = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
999
987
  const healthTimer = setInterval(() => void refreshHealth(), HEALTH_INTERVAL_MS);
1000
988
  const claimLocal = async () => {
@@ -1081,7 +1069,7 @@ const main = async () => {
1081
1069
  } finally {
1082
1070
  clearInterval(heartbeatTimer);
1083
1071
  clearInterval(healthTimer);
1084
- push?.stop();
1072
+ push.stop();
1085
1073
  await stop('shutdown');
1086
1074
  }
1087
1075
  };
package/runtime/push.mjs CHANGED
@@ -1,43 +1,23 @@
1
1
  /* eslint-disable import/extensions -- Native Node ESM requires file extensions. */
2
2
  // Push-driven cloud claims. The queue pushes "work queued" over a reactive
3
- // WebSocket, so the runner claims only when there is work, plus a slow fallback
4
- // claim that covers missed pushes and queues without a push channel.
3
+ // WebSocket, so the runner claims only when there is work. Every (re)connect
4
+ // delivers a fresh snapshot of queued work, so work queued while the socket was
5
+ // down is claimed on reconnect.
5
6
 
6
7
  export const CLUSTER_WORK_PATH = 'cluster.work';
7
- export const DEFAULT_PUSH_FALLBACK_MS = 60_000;
8
- export const DEFAULT_PUSH_TICKET_PATH = '/api/cluster/token';
9
- export const DEFAULT_PUSH_HTTP_PATH = '/api/cluster/reactive';
8
+ const TICKET_PATH = '/api/cluster/token';
9
+ const HTTP_PATH = '/api/cluster/reactive';
10
10
 
11
- /**
12
- * Reads AG_AI_PUSH_FALLBACK_MS. Unset, blank or invalid values keep the default,
13
- * so a typo can never stop the fallback claim.
14
- */
15
- export const parsePushFallbackMs = (value) => {
16
- const milliseconds = Number(value);
17
- return value?.trim() && Number.isSafeInteger(milliseconds) && milliseconds >= 1_000
18
- ? milliseconds
19
- : DEFAULT_PUSH_FALLBACK_MS;
20
- };
21
-
22
- /**
23
- * Decides when a cloud claim is worth a request. Disabled, it always claims, which
24
- * keeps the original polling behaviour.
25
- */
26
- export const createWorkSignal = ({
27
- enabled,
28
- fallbackMs = DEFAULT_PUSH_FALLBACK_MS,
29
- now = Date.now,
30
- }) => {
11
+ /** Decides when a cloud claim is worth a request: on start, and after each push. */
12
+ export const createWorkSignal = () => {
31
13
  // Claim once at start: work may have been queued while the runner was down.
32
14
  let pending = true;
33
- let nextFallbackAt = 0;
34
15
  let wakeWaiter;
35
16
  return {
36
- shouldClaim: () => !enabled || pending || now() >= nextFallbackAt,
37
- /** A claim found no work: wait for a push or the fallback. */
17
+ shouldClaim: () => pending,
18
+ /** A claim found no work: wait for the next push. */
38
19
  drained: () => {
39
20
  pending = false;
40
- nextFallbackAt = now() + fallbackMs;
41
21
  },
42
22
  /** Work was pushed: claim on the next loop, waking an idle wait now. */
43
23
  wake: () => {
@@ -60,18 +40,10 @@ export const createWorkSignal = ({
60
40
 
61
41
  /**
62
42
  * Holds the reactive WebSocket and calls `onWork` whenever the pushed snapshot
63
- * lists queued work. Never throws: failures are logged and the fallback claim
64
- * keeps the runner working.
43
+ * lists queued work. Never throws: failures are logged and the connection keeps
44
+ * retrying, and the reconnect snapshot catches up on anything queued meanwhile.
65
45
  */
66
- export const startPushChannel = ({
67
- baseUrl,
68
- token,
69
- ticketPath = DEFAULT_PUSH_TICKET_PATH,
70
- httpPath = DEFAULT_PUSH_HTTP_PATH,
71
- onWork,
72
- log,
73
- connect,
74
- }) => {
46
+ export const startPushChannel = ({ baseUrl, token, onWork, log, connect }) => {
75
47
  const controller = new AbortController();
76
48
  let client;
77
49
  let subscription;
@@ -81,8 +53,8 @@ export const startPushChannel = ({
81
53
  client = await connectClusterWorker({
82
54
  baseUrl,
83
55
  runnerToken: token,
84
- ticketPath,
85
- httpPath,
56
+ ticketPath: TICKET_PATH,
57
+ httpPath: HTTP_PATH,
86
58
  signal: controller.signal,
87
59
  onRetry: (error) => log(`push connect failed; retrying: ${error.message}`),
88
60
  client: { onError: (error) => log(`push socket error: ${error.message}`) },