@jc_stack/ez-agents 0.1.0-beta.39 → 0.1.0-beta.40

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.40
4
+
5
+ - Apply eligible npm releases under each agent's saved automatic update policy
6
+ through the existing guarded updater. Queue one update at a time, preserve
7
+ manual policies, and wait for a newer version after a failed automatic update.
8
+
3
9
  ## 0.1.0-beta.39
4
10
 
5
11
  - Render completed, binding-scoped application replies as WAV speech with
package/docs/upgrades.md CHANGED
@@ -12,12 +12,13 @@ automatic channel for core and plugins without a saved policy. Existing explicit
12
12
  stable or manual policies are preserved. The owner may select either per target.
13
13
  The main target and installed plugins version independently.
14
14
 
15
- The agent owns release review, policy decisions and communication. The host
16
- supervisor owns interruption-safe replacement. There is one host service per
17
- agent, not a second agent or an additional updater daemon. Its replaceable child
18
- runs the normal CLI transport. It checks npm every six hours while running and
19
- records automatic-channel changes; discovery never wakes the agent or injects
20
- wrapper state into a session. Updates stay owner-invoked through `ez updates`.
15
+ The owner chooses the saved update policy. The host supervisor owns
16
+ interruption-safe replacement. There is one host service per agent, not a second
17
+ agent or an additional updater daemon. Its replaceable child runs the normal CLI
18
+ transport. It checks npm every six hours while running and queues one eligible
19
+ release at a time when the policy is automatic. Discovery and replacement never
20
+ wake the agent or inject wrapper state into a session. Manual policies retain
21
+ owner-invoked updates through `ez updates`.
21
22
  Checks use the installed scoped npm identity; failures are
22
23
  visible in `updates check` and private `tools/updates/available.json`. Plugins marked
23
24
  `private: true` in their package metadata are reported as local-source updates
@@ -265,5 +266,7 @@ execution authority.
265
266
 
266
267
  Update discovery runs independently of active host work. A discovery failure is
267
268
  logged locally and retried at the next regular six-hour check; it does not stop
268
- the host or create a repair task. Inspect `ez updates check` for target diagnostics.
269
+ the host or create a repair task. An automatically failed or rolled-back version
270
+ is not retried until a newer version appears or the owner explicitly retries.
271
+ Inspect `ez updates check` and `ez updates status` for target diagnostics.
269
272
  Actual update transactions retain their existing admission, drain and rollback rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jc_stack/ez-agents",
3
- "version": "0.1.0-beta.39",
3
+ "version": "0.1.0-beta.40",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "A lightweight foundation for persistent business AI assistants using existing AI harnesses, workspaces and plugins.",
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { atomic, locked } from '../plugins/manager.mjs';
6
6
  import { callDeliverySocket } from '../delivery-socket-client.mjs';
7
- import { state, read, jobs, jobPath, check, missing, cleanupStaleBackups } from './control.mjs';
7
+ import { state, read, jobs, jobPath, check, prepare, submit, missing, cleanupStaleBackups } from './control.mjs';
8
8
  import { perform, environment } from './runtime.mjs';
9
9
 
10
10
  const reservedProviderKeys=new Set(['HOME','LANG','LC_ALL','LOGNAME','PATH','SHELL','TERM','TMPDIR','USER','CODEX_HOME','NODE_OPTIONS']);
@@ -28,6 +28,22 @@ export async function idle(control) {
28
28
  const status=await callDeliverySocket(path.join(control,'delivery.sock'),{op:'status'},2000).catch(()=>null);
29
29
  return !status||status.running===0;
30
30
  }
31
+ export async function queueAutomatic(home,available) {
32
+ const existing=await jobs(home);
33
+ if(existing.some(job=>['queued','applying','recovery-required'].includes(job.status)))return null;
34
+ for(const candidate of available) {
35
+ if(!candidate.newer||!candidate.policy?.automatic||!candidate.available)continue;
36
+ // A failed release needs a newer version or an explicit owner retry.
37
+ if(existing.some(job=>job.target===candidate.target&&job.version===candidate.available&&['failed','rolled-back'].includes(job.status)))continue;
38
+ try {
39
+ const job=await prepare(home,candidate.target,{release:candidate.available});
40
+ return await submit(home,job.id,true);
41
+ }catch {
42
+ console.error(`Automatic update of ${candidate.target} failed; inspect ez updates check/status.`);
43
+ }
44
+ }
45
+ return null;
46
+ }
31
47
  export async function supervise(deployment,signal,{discover=check}={}) {
32
48
  const host=await read(path.join(deployment,'host-executor.json'));
33
49
  if(host.agents.length!==1||!host.agents[0].toolsHome)throw Error('Initialize a single deployment plugin registry before starting the supervisor');
@@ -96,12 +112,16 @@ export async function supervise(deployment,signal,{discover=check}={}) {
96
112
  const latest=await read(path.join(jobPath(home,pending.id),'job.json'));
97
113
  if(latest.status==='queued'){latest.status='failed';latest.error=error.message;await atomic(path.join(jobPath(home,pending.id),'job.json'),latest);}else throw error;
98
114
  } finally {await fs.rm(pause,{force:true});}
99
- if(result?.status==='completed'&&pending.target==='main')return;
115
+ if(result?.status==='completed') {
116
+ if(pending.target==='main')return;
117
+ nextCheck=0;
118
+ }
100
119
  }
101
120
  if(Date.now()>=nextCheck) {
102
121
  nextCheck=Date.now()+6*60*60*1000;
103
122
  try {
104
123
  const results=await discover(home);await atomic(path.join(directory,'available.json'),results);
124
+ await queueAutomatic(home,results);
105
125
  } catch {
106
126
  // Discovery is optional; its failure must not terminate the host.
107
127
  // Do not expose registry response bodies or credentials in logs.
@@ -13,7 +13,7 @@ import { perform, environment, packageManager, backupStateDirectory } from '../s
13
13
  import { atomic, snapshot, compose, prepareCommand } from '../src/plugins/manager.mjs';
14
14
  import { bindUpdates } from '../src/updates/binding.mjs';
15
15
  import { status as runtimeStatus } from '../src/updates/status.mjs';
16
- import { providerEnvironment } from '../src/updates/supervisor.mjs';
16
+ import { providerEnvironment, queueAutomatic } from '../src/updates/supervisor.mjs';
17
17
  const exec=promisify(execFile);
18
18
 
19
19
  test('main upgrade backup omits a live plugin socket and keeps private files',async t=>{
@@ -442,6 +442,29 @@ test('npm candidates verify exact version and integrity; automatic policy is enf
442
442
  pkg.version='0.1.2';await assert.rejects(prepare(f.home,'main',{release:'0.1.1'}),/version mismatch/);
443
443
  });
444
444
 
445
+ test('supervisor queues an eligible release once and preserves manual and failed-version gates',async t=>{
446
+ const f=await fixture(t),data=await fs.readFile(await f.pack());
447
+ const {createHash}=await import('node:crypto');
448
+ const pkg={name:'@ez-test/example',version:'0.1.1',dist:{tarball:'https://registry.npmjs.org/@ez-test/example/-/example-0.1.1.tgz',integrity:'sha512-'+createHash('sha512').update(data).digest('base64')}};
449
+ const original=globalThis.fetch;
450
+ globalThis.fetch=async url=>new Response(String(url).endsWith('.tgz')?data:JSON.stringify(String(url).endsWith('/0.1.1')?pkg:{name:pkg.name,'dist-tags':{latest:pkg.version},versions:{[pkg.version]:pkg}}));
451
+ t.after(()=>globalThis.fetch=original);
452
+ await atomic(path.join(f.home,'updates/supervisor.json'),{at:Date.now()});
453
+ await command(f.home,['policy','main','manual']);
454
+ assert.equal(await queueAutomatic(f.home,await command(f.home,['check'])),null);
455
+ assert.deepEqual(await jobs(f.home),[]);
456
+ await command(f.home,['policy','main','beta']);
457
+ const queued=await queueAutomatic(f.home,await command(f.home,['check']));
458
+ assert.equal(queued.status,'queued');assert.equal(queued.automatic,true);
459
+ assert.equal(queued.origin.type,'npm');assert.equal(queued.sha256,digest(data));
460
+ assert.equal(await queueAutomatic(f.home,await command(f.home,['check'])),null);
461
+ assert.equal((await jobs(f.home)).length,1);
462
+ queued.status='failed';await atomic(path.join(jobPath(f.home,queued.id),'job.json'),queued);
463
+ assert.equal(await queueAutomatic(f.home,await command(f.home,['check'])),null);
464
+ assert.equal((await jobs(f.home)).length,1);
465
+ await assert.rejects(fs.readFile(path.join(f.agent.controlDir,'update-attention.json')),error=>error.code==='ENOENT');
466
+ });
467
+
445
468
  test('update discovery follows latest while preserving legacy beta and stable-only policies',async t=>{
446
469
  const {registryCandidate}=await import('../src/updates/artifact.mjs');
447
470
  const name='@ez-test/example',pkg=v=>({name,version:v});