@jc_stack/ez-agents 0.1.0-beta.52 → 0.1.0-beta.58

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.58
4
+
5
+ Versions 0.1.0-beta.53 through 0.1.0-beta.57 were used only by private QA candidates; this release follows the newest installed candidate (0.1.0-beta.58.rc.1) so it can upgrade in place.
6
+
7
+ - Add an owner chat/application fallback chain (`ezenciel-agents-ai fallbacks`), captured when work is queued, with the scheduled-fallback safeguards: advance only on typed unavailability, rejection before work or a usage limit, never after cancellation, access denial, revoked authority or an uncertain outcome. Same-CLI logins keep the turn's model and effort, and each login keeps its own native session per conversation.
8
+ - Add opt-in per-attempt run telemetry (`ezenciel-agents-ai telemetry on`): reported model, deduplicated token counts, tool counts/durations and error categories with labelled coverage gaps, never prompts, tool data, credentials or costs.
9
+ - Make schedule edits safe for queued and running work. `edit` now requires `--expected-revision` and rejects a stale revision. It applies only while the task is idle, or with `--stage` saves the change for future occurrences while queued and running ones finish on their captured settings, including fallbacks and quota retries. Edits, dispatch, trigger, pause, resume and remove serialize on a per-task lock, so a due occurrence cannot land between the check and the write. Edit needs the running relay. A root invocation of the schedule CLI runs as the control directory's owner, so its definitions stay readable by the relay and it cannot follow agent-planted symlinks.
10
+ - Hold new native turn admission while a completed or rolled-back core update drains for supervisor reload. Retain queued requests and reopen only after the replacement worker is ready.
11
+ - Preserve scheduled fallback handling when a compatible relay upgrade adopts a running native session, including continuation after a terminal usage limit without replaying its work.
12
+ - Reuse the last successful same-model native login across scheduled tasks and relay restarts; rotate through configured logins and skip known exhausted scopes until their provider reset.
13
+ - Keep scheduled work queued when every saved CLI login reaches its usage limit, instead of failing on each tick. Known resets wake the occurrence; unknown resets back off from 5 minutes to hourly. Codex reports native rate limits, and Claude credits-required responses wait for an explicit retry. `ezenciel-agents-schedule retry [RUN_ID]` and `/retry_quota [RUN_ID]` clear the relevant cooldowns after a manual reset or added credits.
14
+ - Add `ezenciel-agents-schedule running [--json]`, a read-only view of queued and running current-owner foreground turns and scheduled runs with their engine, timing and schedule origin. It exports no prompts, messages or native output.
15
+ - Restore isolated Codex chat replies: the isolated executor derives the Codex sandbox setting from its trusted deployment environment, never from an incoming job.
16
+ - Keep unfinished Claude background work alive in print mode until completion or explicit cancellation, instead of being killed after 10 idle minutes.
17
+
18
+ Beta limits: live fresh-host/reboot acceptance and live installed-agent QA of the chat fallback chain, run telemetry and safe schedule edits remain pending; they are covered by synthetic relay, CLI and container tests. A Codex chat setup advances only when its CLI is unavailable, so put Codex last in a chat chain. On its first turn, a fallback login's linked session does not see the conversation's earlier native history. Run telemetry and quota-waiting runs live in relay memory and reset on restart; saved-login cooldowns persist. Callers of `ezenciel-agents-schedule edit` must now pass `--expected-revision`, and edit needs the running relay. Publication does not establish an installed-agent upgrade or authorize provider/domain writes.
19
+
3
20
  ## 0.1.0-beta.52
4
21
 
5
22
  - Restore isolated executor startup with existing named volumes and a private, root-owned read-only container binding. Host file ownership and permissions remain unchanged.
package/README.md CHANGED
@@ -333,4 +333,7 @@ The core `ezenciel-agents-schedule` CLI accepts instruction text for one-off dat
333
333
  intervals and timezone-aware cron schedules. `create --now` delegates a task to a
334
334
  separate CLI session queued with owner turns in the bound workspace. Long work has
335
335
  no production wall-clock timeout; goals and subagents remain native executor
336
- features. See [scheduling, recovery and QA](docs/scheduling.md).
336
+ features. Use `ezenciel-agents-schedule running` for a read-only view of queued
337
+ and running foreground/scheduled work, or add `--json` for an agent-readable
338
+ snapshot. Native child status and tool activity are unavailable; old or missing
339
+ telemetry does not imply idle or complete. See [scheduling, recovery and QA](docs/scheduling.md).
@@ -2,7 +2,10 @@
2
2
 
3
3
  `menu.ts` owns Telegram controls; `ai.ts` projects installed-client metadata.
4
4
  `ControlStore` stores saved presets, the next-conversation default and current choice.
5
- No provider SDK, model fallback, history migration, or second execution loop.
5
+ No provider SDK, history migration, or second execution loop. The only model
6
+ fallback is an owner-saved ordered chain captured with each turn and advanced by
7
+ the relay's existing attempt loop on typed evidence
8
+ ([chat fallback chain](../docker-runtime.md#chat-fallback-chain)).
6
9
  A missing catalog entry rejects selection without changing the conversation.
7
10
  An unsupported effort reports the exact installed model's advertised levels.
8
11
  Missing selectable models do not establish that the active native session is
@@ -19,7 +22,8 @@ and native history; subsequent turns pass the new settings to the native engine.
19
22
  Changing client, provider or auth profile starts a separate native conversation.
20
23
  A conversation records its client and auth profile when created; launch rejects a
21
24
  choice for another profile, so a native session never resumes under another
22
- account. Provisioned profiles appear as additional catalog entries for their
25
+ account; a fallback login uses its own linked native session for the
26
+ conversation. Provisioned profiles appear as additional catalog entries for their
23
27
  client (see [auth profiles](../docker-runtime.md#auth-profiles)). Old bindings
24
28
  remain available so accepted work can finish and owners can reopen their chats.
25
29
 
@@ -286,6 +286,9 @@ On macOS host jobs, Seatbelt also denies sibling directories of the bound
286
286
  workspace (a tenant farm), while allowing explicitly shared and additional
287
287
  workspace grants and leaving `$HOME` readable for the host CLI.
288
288
  Codex then uses `danger-full-access` so it does not apply a nested Seatbelt.
289
+ The isolated executor derives the Codex sandbox setting from its trusted
290
+ deployment environment, never from an incoming job. Host workers and restricted
291
+ tasks retain their existing native sandbox.
289
292
 
290
293
  ## Claude Code auth
291
294
 
@@ -338,6 +341,51 @@ the profile entries in Choose AI, or `ezenciel-agents-schedule create ...
338
341
  profile starts a new native conversation, and a run whose request names another
339
342
  profile than its captured choice is rejected before launch.
340
343
 
344
+ ## Chat fallback chain
345
+
346
+ Chat and application turns can name an ordered chain after the selected AI, for
347
+ example Claude default → three other Claude logins → Codex:
348
+
349
+ ```sh
350
+ ezenciel-agents-ai fallbacks --fallback cli=claude,auth-profile=claude2 \
351
+ --fallback cli=claude,auth-profile=claude3 --fallback cli=claude,auth-profile=claude4 \
352
+ --fallback cli=codex,model=gpt-6.1-sol,effort=xhigh
353
+ ```
354
+
355
+ An entry for the selected AI's CLI without provider, model or effort keeps the
356
+ turn's own model and effort, so every role keeps its settings on other logins.
357
+ Each queued turn captures the chain with its choice; later edits apply to later
358
+ turns only. `fallbacks` alone shows the resulting order and `--clear` removes it.
359
+ Profiles must be provisioned and models advertised before they are saved.
360
+
361
+ The rules of [ordered setup fallback](scheduling.md#ordered-setup-fallback)
362
+ apply: core advances only on typed evidence that a setup was unavailable or
363
+ rejected before work, or stopped at its usage limit (the next setup is told the
364
+ work may be partly applied). Access denial, other failures after work began,
365
+ unknown outcomes, owner stop and revoked owner or application authority end the
366
+ turn without replay. A login cooling down after a recorded quota rejection is
367
+ skipped while another is available; when all are, they are tried in order.
368
+ Codex chat turns report no typed usage-limit rejection, so a Codex setup
369
+ advances only when its CLI is unavailable; put Codex last in the chain.
370
+ Each login keeps its own native session for the conversation, created on first
371
+ use and resumed on later turns; a session never resumes under another login. A
372
+ fallback login's first turn does not see the conversation's earlier native
373
+ history; durable context lives in the workspace.
374
+
375
+ ## Run telemetry
376
+
377
+ `ezenciel-agents-ai telemetry on` (off by default) records, for every attempt
378
+ started afterwards, the model the client reported, input/output/cache-read/
379
+ cache-write token counts, tool call counts, errors and observed durations by
380
+ tool name, and error categories. They appear in each attempt of
381
+ `ezenciel-agents-schedule run RUN_ID`. Counts come from structured client
382
+ events: repeated per-message usage is counted once, a client's cumulative total
383
+ replaces partial sums, and `coverage`/`gaps` label anything missing or partial,
384
+ such as a resumed Codex session whose total may include earlier turns. Prompts,
385
+ model text, tool arguments and results, credentials and reasoning are never
386
+ read into telemetry, and no cost or historical usage is derived. Records stay
387
+ in relay memory with the run and reset on restart.
388
+
341
389
  ## OpenCode auth
342
390
 
343
391
  Isolated agents run the image OpenCode with `XDG_DATA_HOME` set to the
@@ -13,6 +13,8 @@ ezenciel-agents-schedule create --name Research --now \
13
13
  --text '/goal Complete the authorized research objective. Save evidence, verify the outcome, and send the owner the result.'
14
14
  ezenciel-agents-schedule list
15
15
  ezenciel-agents-schedule runs
16
+ ezenciel-agents-schedule running
17
+ ezenciel-agents-schedule running --json
16
18
  ezenciel-agents-schedule trigger SCHEDULE_ID --key owner-smoke-20261003
17
19
  ezenciel-agents-schedule pause SCHEDULE_ID
18
20
  ezenciel-agents-schedule resume SCHEDULE_ID
@@ -20,13 +22,75 @@ ezenciel-agents-schedule remove SCHEDULE_ID
20
22
  ezenciel-agents-schedule cancel RUN_ID
21
23
  ```
22
24
 
23
- Use `--text-file` for longer instructions. `edit ID` replaces the complete schedule
24
- with a new revision; use the same trigger/name/text flags as `create`. It keeps
25
- the paused/enabled state: an edited paused task stays paused until `resume`.
25
+ Use `--text-file` for longer instructions. `edit ID --expected-revision REVISION`
26
+ replaces the complete schedule with a new revision; use the same trigger/name/text
27
+ flags as `create`. `REVISION` is the `revision` printed by `show`, `list`,
28
+ `create` or a previous `edit`. If the schedule changed since that revision, the
29
+ edit is rejected and nothing is written: read it again and reapply the change.
30
+ It keeps the paused/enabled state: an edited paused task stays paused until `resume`.
26
31
  An edit keeps a future pending occurrence when its trigger is unchanged;
27
32
  otherwise the edited rule begins at its next future occurrence. Editing does
28
33
  not replay missed work from an old start date. Use `--now` to request a new
29
34
  one-time run explicitly. `show` reports the pending occurrence used by dispatch.
35
+
36
+ An edit never disrupts an occurrence that is already queued or running:
37
+
38
+ - Without `--stage`, it applies only while the task is idle. When an occurrence
39
+ is queued (including a quota wait) or running, it is rejected with those run IDs
40
+ and nothing changes. Edit again after they finish, or `cancel RUN_ID` first.
41
+ - With `--stage`, the new revision is saved for future occurrences. The queued or
42
+ running occurrences listed in `continuingRunIds` finish with the instructions,
43
+ engine, model, effort, fallbacks, delivery binding and revision they captured,
44
+ including fallback advancement and quota retries. The next occurrence of the new
45
+ revision waits for them, as usual for one task. A continuing occurrence that is
46
+ interrupted (or fails, for a `--when unreviewed-failures` schedule) holds the
47
+ new revision until the next explicit edit.
48
+
49
+ The compare, idle check and write happen under the task's lock, which dispatch,
50
+ `trigger`, `pause`, `resume`, `remove` and quota `retry` also take; a due occurrence therefore
51
+ lands either before the edit (and is protected as above) or after it, on the new
52
+ revision. Edit needs the running relay to read queued and running occurrences;
53
+ an unavailable relay rejects the edit rather than assuming the task is idle.
54
+ `list`, `show`, `create`, `pause`, `resume`, `remove` and script registration keep
55
+ working with the relay stopped.
56
+
57
+ Definitions are private files owned by the agent runtime user. A root invocation,
58
+ such as `docker compose exec relay ezenciel-agents-schedule ...`, first switches to
59
+ the user and group that own the control directory, so saved definitions stay
60
+ readable by the relay and root never writes through paths the agent controls.
61
+
62
+ `running` gives one read-only view of **queued and running current-owner
63
+ foreground turns (Telegram/application) and scheduled runs** in the bound relay.
64
+ It shows run IDs,
65
+ schedule names/IDs/revisions, the saved or latest attempted engine/model, start and
66
+ elapsed times, and the last recorded relay activity. `--json` returns the same
67
+ sanitized metadata with `schemaVersion`, `observedAt`, `counts`, `coverage` and
68
+ `runs`, suitable for asking the agent to summarize what is running. Other commands
69
+ keep their existing JSON output. The relay must be reachable; an unavailable
70
+ relay is an error, never an empty/idle result.
71
+
72
+ Activity means a recorded queue/start, setup attempt or delivery enqueue time;
73
+ it does not measure native tool progress. An old observation does not change a
74
+ run's recorded status. Missing start/model/activity metadata stays unknown, and
75
+ finished runs are omitted. Schedule names are available only when the saved
76
+ definition still matches that run's revision. For ordered fallbacks, the latest
77
+ non-skipped attempt is shown with its recorded outcome. Script runs have no
78
+ engine/model.
79
+
80
+ `origin` and `scheduledChildren` link scheduled work to its recorded originating
81
+ request. An origin may already be finished; its status is unknown when its record
82
+ is outside retained owner coverage. These links are request provenance, not a
83
+ native subagent tree. Native child status and native tool activity are unavailable
84
+ on the current adapters, including Codex, Codex desktop, Claude, Grok, OpenCode
85
+ and Pi. Native sessions, provider quotas and process lists are not queried.
86
+ Prompts, message text, script arguments and raw engine output are not exported.
87
+
88
+ This view covers one relay's retained memory. Ordinary restarts drop the ledger;
89
+ gracefully adopted host runs are the existing exception. Standalone manual
90
+ CLI/desktop sessions, other installations and restricted external/isolated tasks
91
+ are outside this view. Application request context is omitted. Future schedule
92
+ occurrences that have not been queued remain in `list`/`show`.
93
+
30
94
  Intervals use `--every-seconds` (minimum 60), optional `--start`, and optional
31
95
  `--until`. Cron also supports start/end bounds. All absolute timestamps require
32
96
  an explicit offset. `--now` starts on the next relay tick, not synchronously.
@@ -92,20 +156,33 @@ error, unavailable model or bad request), including a missing, expired or revoke
92
156
  login (typed authentication failure or HTTP401). Native API-error metadata does
93
157
  not count as model/tool work. It stops, without replay, on access
94
158
  denial (HTTP403, account or organization restriction), cancellation, relay shutdown,
95
- a schedule edit, a policy or unknown error, an interrupted transport, or any
159
+ schedule removal, a policy or unknown error, an interrupted transport, or any
96
160
  other failure after work began. Evidence comes from structured events only: Codex
97
161
  native-session `codexErrorInfo` and Claude `stream-json` error fields (scheduled
98
162
  Claude runs use `--output-format stream-json`; the host forwards only a compact
99
163
  startup summary). Other engines advance only when their CLI is unavailable.
100
164
 
101
- The one failure after work began that continues is a Claude usage limit the run
102
- ended on (for example "You've hit your session limit"): a typed quota rejection
103
- with no model, tool or hook activity after it. A native session cannot be resumed
165
+ After work begins, only a typed Codex or Claude usage limit, or a Claude
166
+ credits-required rejection, permits continuation. The run must have ended on
167
+ that rejection, with no subsequent model, tool or hook activity. A native session cannot be resumed
104
168
  under another login's configuration home, so the next eligible setup starts a fresh
105
169
  session with the saved prompt plus one factual line naming the run, the stopped
106
170
  setup, its native session and reset time, and that its work may be partly applied.
107
171
  The agent reconciles from its own records; core replays nothing else.
108
172
 
173
+ These same completion and fallback rules apply after a graceful relay upgrade
174
+ adopts a running native session. Adoption attaches to that session; only an
175
+ eligible terminal failure starts the next saved setup.
176
+
177
+ For consecutive primary choices that differ only by native login profile, later
178
+ scheduled runs start with the last successfully completed login and rotate through
179
+ the remaining saved logins if it becomes unavailable. The existing owner control
180
+ state retains that preference and typed quota reset times across relay restarts.
181
+ Other tasks with saved fallbacks for those logins skip an exhausted scope until its reset;
182
+ unknown reset times use progressively spaced checks (5, 15, 30, then 60 minutes). A concurrent older run's
183
+ success does not erase a future reset. Saved models, effort, heterogeneous fallback
184
+ priority and chat selection are preserved; no unconfigured login is added.
185
+
109
186
  A login or quota rejection skips the remaining setups in the same scope: the native
110
187
  client and login profile (`claude`, `codex` including desktop and custom Codex
111
188
  providers), `grok`, or the upstream provider for OpenCode/Pi models. A named
@@ -113,17 +190,47 @@ profile selects a separate owner-provisioned account and is eligible for its own
113
190
  attempt; changing only the model in the same profile does not reset its scope.
114
191
  Do not provision aliases of the same account to obtain more allowance. Ez does
115
192
  not infer account independence from credentials or promise that a shared
116
- organization's allowance is separate. Each setup runs at most once per occurrence.
193
+ organization's allowance is separate. Each setup runs at most once per availability check; quota waits retain the earlier attempts.
117
194
 
118
195
  Every attempt keeps the same run ID, occurrence, schedule revision, workspace,
119
- delivery binding and scheduled concurrency slot, in a fresh native session.
196
+ delivery binding, in a fresh native session. Quota waits release the scheduled concurrency slot.
120
197
  `run RUN_ID` shows `attempts`: each setup, quota scope, outcome, failure category
121
198
  (`cli-unavailable`, `login-unavailable`, `quota`, `provider-rejected`, `access-denied`,
122
- `quota-after-work-began`, `after-work-began`, `uncertain`, or `same-login-scope`/`same-quota-scope`
123
- for a skipped setup), exit code, reported reset time and the session ID Ez assigned. If every setup is unavailable the run fails with
124
- reason `setups-unavailable`, a summary of each setup, and an owner notice on
125
- Telegram-bound tasks. Core never purchases credits, enables overage or consumes
126
- reset credits; the next regular occurrence runs normally.
199
+ `quota-after-work-began`, `credits-required`, `credits-required-after-work-began`, `after-work-began`, `uncertain`, or `same-login-scope`/`same-quota-scope`
200
+ for a skipped setup), exit code, reported reset time and the session ID Ez assigned.
201
+
202
+ When all saved logins are quota-blocked, the same occurrence stays `queued` with
203
+ `quotaWait`. A known reset schedules its next availability check. If it passed during native
204
+ shutdown, one prompt probe precedes ordinary backoff. Unknown resets
205
+ back off per login to an hourly maximum. A credits-required rejection waits for
206
+ explicit retry. Other unavailable setups still fail with `setups-unavailable`.
207
+ Waiting consumes no native process or scheduled execution slot. The scheduler
208
+ keeps one pending occurrence per task, so repeated ticks do not accumulate copies.
209
+ Waiting occurrences share one probe per login until native work is accepted.
210
+ `/status` and the queued/running-work view show waiting and the next check.
211
+
212
+ After adding credits or manually resetting a provider limit, use either:
213
+
214
+ ```sh
215
+ ezenciel-agents-schedule retry # wake current queued quota waits
216
+ ezenciel-agents-schedule retry RUN_ID # retry this waiting or prior quota-failed occurrence
217
+ ```
218
+
219
+ Telegram: `/retry_quota` or `/retry_quota RUN_ID` (`/retry-quota` also works).
220
+ The command works without an inference turn. It clears the cooldowns for the
221
+ occurrence's saved login scopes and requests a new availability check. It never
222
+ adds a login, changes the model or buys credits. A specific prior run is eligible
223
+ only when its failure was entirely quota-related, its schedule is current and
224
+ enabled, and no other occurrence of that task is queued or running. Cancelled,
225
+ interrupted, edited, foreign-owner and other failed runs cannot be retried this way.
226
+ `/retry` continues to handle failed incoming messages.
227
+
228
+ Prior attempts and stopped native-session references remain in the same run.
229
+ After partial work, the engine receives the existing factual continuation line
230
+ and reconciles effects from its own records. Core reconstructs no history.
231
+ Cooldowns persist in owner control state; waiting run records follow the existing
232
+ relay-memory retention contract and do not survive a restart. Core never purchases
233
+ credits, enables overage or consumes reset credits.
127
234
 
128
235
  ## Registered script schedules
129
236
 
@@ -249,8 +356,9 @@ Schedule definitions and cursors use protected atomic JSON files. A restart
249
356
  between queue creation and cursor persistence does not duplicate an occurrence.
250
357
  An overdue one-time job runs on return. Missed recurring occurrences coalesce into
251
358
  one pending run; future runs resume at the next eligible time. A paused queued
252
- occurrence waits for resume. Editing/removing a schedule invalidates its old
253
- queued occurrence; already-running work continues until explicitly cancelled.
359
+ occurrence waits for resume. Removing a schedule invalidates its queued
360
+ occurrence; already-running work continues until explicitly cancelled. An edit
361
+ never invalidates queued or running work (see `edit` above).
254
362
 
255
363
  `/stop` stops all active work; `cancel RUN_ID` stops one background task. `/cancel`
256
364
  clears queued work. Pause/remove a recurring schedule to prevent future runs.
package/docs/upgrades.md CHANGED
@@ -16,7 +16,7 @@ automatic channel for core and plugins without a saved policy. Existing explicit
16
16
  stable or manual policies are preserved. The owner may select either per target.
17
17
  The main target and installed plugins version independently.
18
18
 
19
- Handoff-compatible core upgrades build while native work continues and pause admission only for activation. The current native executor retires from claiming new work; the replacement claims new requests while the retiring generation finishes its existing runs. The relay adopts those runs without replay, preserving their schedule holds, cancellation and settled delivery receipts. The supervisor reloads only after native work is idle, so OS service process-group cleanup cannot kill a retiring generation. Plugin upgrades build while native work continues. Activation closes the existing registry admission lock, drains in-flight plugin command leases, and replaces the plugin under that lock. New native plugin calls wait for admission; an outdated revision fails before provider execution and must be rediscovered. No admitted provider operation is replayed. The shared broker and native sessions remain running. Older runtimes without the handoff contract retain the full native-work drain. Isolated agents migrate once after live native work finishes to a separate, unprivileged executor container. Pending requests remain queued for that executor; its private binding is copied into the container as root-owned, group-readable and not writable by the configured runtime user. Existing named volumes are attached as external volumes. It uses the same file transport; later relay/runtime upgrades preserve that executor generation. No native CLI or provider credentials move to the host. A plugin update also retains that drain if an operator replaced the broker’s single-file host binding (for example to approve new network pins), because that case still requires broker recreation.
19
+ Handoff-compatible core upgrades build while native work continues; activation and the later supervisor reload hold new turn admission. The current native executor retires from claiming new work; the replacement claims new requests while the retiring generation finishes its existing runs. The relay adopts those runs without replay, preserving their schedule holds, cancellation and settled delivery receipts. After completion or rollback, the supervisor holds new turn admission until existing native work finishes and its successor worker is ready. Queued requests remain in the running relay and file transport; they do not block this supervisor reload. OS service process-group cleanup therefore cannot kill a retiring generation or be starved by newly admitted work. Plugin upgrades build while native work continues. Activation closes the existing registry admission lock, drains in-flight plugin command leases, and replaces the plugin under that lock. New native plugin calls wait for admission; an outdated revision fails before provider execution and must be rediscovered. No admitted provider operation is replayed. The shared broker and native sessions remain running. Older runtimes without the handoff contract retain the full native-work drain. Isolated agents migrate once after live native work finishes to a separate, unprivileged executor container. Pending requests remain queued for that executor; its private binding is copied into the container as root-owned, group-readable and not writable by the configured runtime user. Existing named volumes are attached as external volumes. It uses the same file transport; later relay/runtime upgrades preserve that executor generation. No native CLI or provider credentials move to the host. A plugin update also retains that drain if an operator replaced the broker’s single-file host binding (for example to approve new network pins), because that case still requires broker recreation.
20
20
 
21
21
  Compatible main upgrades also finish their advisory workspace/native-client
22
22
  backup before closing admission or stopping the relay. Large native histories do
@@ -63,10 +63,15 @@ restart and verify. Do not recreate the agent or its registry. Subsequent
63
63
  compatible upgrades are agent-owned. Noncanonical/multi-agent host configuration
64
64
  files require migration to one canonical deployment per service first.
65
65
 
66
- ## Recovering an isolated-executor startup defect
66
+ ## Recovering an isolated-executor or supervisor admission defect
67
67
 
68
- If an older supervisor cannot start its isolated executor, queued requests can
69
- prevent that same supervisor from installing its correction. Use an exact,
68
+ The post-update reload requires a live relay status response taken after
69
+ in-progress admissions finish. An unavailable or timed-out response keeps
70
+ admission closed and leaves active executors running.
71
+
72
+ If an older supervisor cannot start its isolated executor, or its post-update
73
+ reload admits new work indefinitely, it can prevent its own correction from
74
+ being installed. Use an exact,
70
75
  reviewed newer package's `src/updates/supervisor.mjs DEPLOYMENT_DIRECTORY` as the
71
76
  existing OS service's temporary entry point. First verify that no native run is
72
77
  active. Keep the same service user, environment, registry, deployment and queued
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jc_stack/ez-agents",
3
- "version": "0.1.0-beta.52",
3
+ "version": "0.1.0-beta.58",
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.",
package/src/ai-cli.ts CHANGED
@@ -2,15 +2,15 @@ import { parseArgs } from 'node:util'
2
2
  import { randomBytes } from 'node:crypto'
3
3
  import { join } from 'node:path'
4
4
  import { mkdir, readFile } from 'node:fs/promises'
5
- import { authProfileStatus, readModels, presetProvider, sameChoice, sameEngine, validateSelection, type AiPreset, type ModelChoice } from './ai.js'
6
- import { cliHome } from './auth-profile.js'
5
+ import { authProfileStatus, readModels, presetLabel, presetProvider, resolveFallbacks, sameChoice, sameEngine, validateSelection, type AiPreset, type ModelChoice } from './ai.js'
6
+ import { cliHome, provisionedHome } from './auth-profile.js'
7
7
  import { ControlStore } from './control-state.js'
8
8
  import { resolveIsolation } from './isolation.js'
9
9
 
10
- const usage='Usage: ezenciel-agents-ai list | select --cli <installed-cli> [--auth-profile <name>] [--provider <provider>] [--model <model>] [--effort <effort>] | default | profiles | profile add --cli codex|claude --auth-profile <name>'
11
- const {values,positionals}=parseArgs({allowPositionals:true,options:{help:{type:'boolean'},cli:{type:'string'},'auth-profile':{type:'string'},provider:{type:'string'},model:{type:'string'},effort:{type:'string'}}})
10
+ const usage='Usage: ezenciel-agents-ai list | select --cli <installed-cli> [--auth-profile <name>] [--provider <provider>] [--model <model>] [--effort <effort>] | default | profiles | profile add --cli codex|claude --auth-profile <name> | fallbacks [--fallback cli=CLI[,auth-profile=NAME][,provider=ID][,model=MODEL][,effort=EFFORT] ... | --clear] | telemetry [on|off]'
11
+ const {values,positionals}=parseArgs({allowPositionals:true,options:{help:{type:'boolean'},cli:{type:'string'},'auth-profile':{type:'string'},provider:{type:'string'},model:{type:'string'},effort:{type:'string'},fallback:{type:'string',multiple:true},clear:{type:'boolean'}}})
12
12
  if(values.help){
13
- console.log(`${usage}\nChoose only values returned by list. Selection affects subsequent messages; default makes the selected AI the choice for new conversations. Queued work is unchanged.\nAuth profiles are separate owner-provisioned Codex/Claude logins with their own configuration and sessions; changing profile starts a new conversation. profile add creates an empty home and prints the native login command for the owner to run; profiles reports read-only login status without secrets.`)
13
+ console.log(`${usage}\nChoose only values returned by list. Selection affects subsequent messages; default makes the selected AI the choice for new conversations. Queued work is unchanged.\nAuth profiles are separate owner-provisioned Codex/Claude logins with their own configuration and sessions; changing profile starts a new conversation. profile add creates an empty home and prints the native login command for the owner to run; profiles reports read-only login status without secrets.\nfallbacks sets the ordered chain tried after the selected AI for chat and application turns, captured when work is queued. An entry for the selected AI's CLI without provider, model or effort keeps the turn's own model and effort. Core advances only when a setup is unavailable or rejected before work, or stopped at its usage limit (a Codex chat setup advances only when its CLI is unavailable); each other login resumes its own native session for that conversation. Without options it shows the chain.\ntelemetry on records per-attempt model, token, tool and error-category counts in run records (ezenciel-agents-schedule run RUN_ID); off by default; never prompts, tool arguments/results or costs.`)
14
14
  process.exit(0)
15
15
  }
16
16
  if (!process.env.EZ_CONTROL_DIR) throw new Error('Use this agent’s bound control directory')
@@ -29,6 +29,32 @@ else if(positionals[0]==='profile' && positionals[1]==='add'){
29
29
  : `CLAUDE_CONFIG_DIR=${home} claude auth login (or store a \`claude setup-token\` token in ${home}/oauth-token, mode 600)`
30
30
  console.log(JSON.stringify({cli:values.cli,authProfile,home,created:created!==undefined,ownerLogin:login}))
31
31
  }
32
+ else if(positionals[0]==='fallbacks'){
33
+ const control=new ControlStore(controlDir,900000)
34
+ if(values.fallback && values.clear)throw new Error('Choose --fallback or --clear')
35
+ if(values.fallback){
36
+ const chain=[]
37
+ for(const [index,spec] of values.fallback.entries()){
38
+ const fields=Object.fromEntries(spec.split(',').map(part=>{const at=part.indexOf('=');if(at<1)throw new Error('Fallback uses cli=CLI[,auth-profile=NAME][,provider=ID][,model=MODEL][,effort=EFFORT]');return [part.slice(0,at),part.slice(at+1)]}))
39
+ if(!fields.cli || Object.keys(fields).some(key=>!['cli','auth-profile','provider','model','effort'].includes(key)))throw new Error('Fallback uses cli=CLI[,auth-profile=NAME][,provider=ID][,model=MODEL][,effort=EFFORT]')
40
+ const preset:AiPreset={id:`fallback-${index+1}`,name:'Fallback',cli:fields.cli,...(fields.provider ? {provider:fields.provider} : {}),...(fields['auth-profile'] ? {authProfile:fields['auth-profile']} : {}),...(fields.model ? {model:fields.model} : {}),...(fields.effort ? {effort:fields.effort} : {})}
41
+ if(preset.authProfile)await provisionedHome(controlDir,preset.cli,preset.authProfile)
42
+ await validateSelection(preset,await catalog())
43
+ chain.push({...preset,name:presetLabel(preset).slice(0,80)})
44
+ }
45
+ await control.setChatFallbacks(chain)
46
+ } else if(values.clear) await control.setChatFallbacks(undefined)
47
+ const state=await control.status()
48
+ const selected=state.ai?.presets.find(p=>p.id===state.ai?.selectedId)
49
+ console.log(JSON.stringify({fallbacks:state.ai?.fallbacks ?? [],...(selected ? {selected:presetLabel(selected),order:[selected,...resolveFallbacks(selected,state.ai?.fallbacks) ?? []].map(presetLabel)} : {}),
50
+ applies:'chat and application work queued afterwards; queued work keeps its captured chain'}))
51
+ }
52
+ else if(positionals[0]==='telemetry'){
53
+ const control=new ControlStore(controlDir,900000)
54
+ if(positionals[1]==='on' || positionals[1]==='off')await control.setRunTelemetry(positionals[1]==='on')
55
+ else if(positionals[1]!==undefined)throw new Error(usage)
56
+ console.log(JSON.stringify({runTelemetry:(await control.status()).runTelemetry===true ? 'on' : 'off',applies:'runs started afterwards'}))
57
+ }
32
58
  else if(positionals[0]==='default'){
33
59
  const control=new ControlStore(controlDir,900000)
34
60
  const state=await control.status()
package/src/ai.ts CHANGED
@@ -10,7 +10,8 @@ import { desktopCodexPath } from './desktop-bridge.js'
10
10
  import { AUTH_PROFILE_CLIS, claudeAuthEnvironment, cliHome, isAuthProfile, listAuthProfiles } from './auth-profile.js'
11
11
 
12
12
  export type AiPreset = { id: string; name: string; cli: string; provider?: string; authProfile?: string; model?: string; effort?: string }
13
- // Optional ordered fallbacks are tried only for a scheduled occurrence that never started work.
13
+ // Ordered fallback setups (scheduled tasks, or the owner's chat chain captured
14
+ // when work is queued) retain the native rejection/partial-work boundaries.
14
15
  export type ExecutionChoice = { sessionId: string; preset: AiPreset; fallbacks?: AiPreset[] }
15
16
  export type ModelChoice = { cli: string; provider?: string; authProfile?: string; model?: string; name: string; efforts: string[] }
16
17
  type Engine = { cli?: string; authProfile?: string }
@@ -35,6 +36,21 @@ export const isExecutionChoice = (v: unknown): v is ExecutionChoice => {
35
36
  return Boolean(c && /^[0-9a-f-]{36}$/i.test(c.sessionId) && isPreset(c.preset) &&
36
37
  (c.fallbacks === undefined || (Array.isArray(c.fallbacks) && c.fallbacks.length > 0 && c.fallbacks.every(isPreset))))
37
38
  }
39
+ // The owner's chat fallback chain for one captured primary choice. A same-client
40
+ // entry without provider, model or effort keeps the primary's settings, so other
41
+ // logins preserve each conversation's or role's model and effort. Entries equal
42
+ // to an earlier setup are dropped.
43
+ export const resolveFallbacks = (primary: AiPreset, chain: AiPreset[] = []): AiPreset[] | undefined => {
44
+ const resolved: AiPreset[] = []
45
+ for (const entry of chain) {
46
+ const inherit = entry.cli === primary.cli && entry.provider === undefined && entry.model === undefined && entry.effort === undefined
47
+ const { provider, model, effort } = inherit ? primary : entry
48
+ const preset: AiPreset = { id: entry.id, name: entry.name, cli: entry.cli, ...(provider ? { provider } : {}),
49
+ ...(entry.authProfile ? { authProfile: entry.authProfile } : {}), ...(model ? { model } : {}), ...(effort ? { effort } : {}) }
50
+ if (![primary, ...resolved].some(other => sameChoice(other, preset))) resolved.push({ ...preset, name: presetLabel(preset).slice(0, 80) })
51
+ }
52
+ return resolved.length ? resolved : undefined
53
+ }
38
54
  export const presetLabel = (p: AiPreset) => `${p.cli}${p.authProfile ? `@${p.authProfile}` : ''}${p.provider ? ` (${p.provider})` : ''} · ${p.model || 'client default'} · ${p.effort || 'default effort'}`
39
55
  // OpenCode encodes its provider in the native provider/model identifier.
40
56
  export const presetProvider = (p: AiPreset) => p.cli === 'opencode' ? p.model?.split('/')[0] : p.provider
@@ -6,8 +6,9 @@ import { fileURLToPath } from 'node:url'
6
6
  import path from 'node:path'
7
7
  import { terminateJob, validateCodexProvider, type CodexProviderBinding } from './executor.js'
8
8
  import { codexRejection, codexResetAt, startupLine } from './setup-fallback.js'
9
+ import { telemetryObserver } from './run-telemetry.js'
9
10
 
10
- type Options = {workspace:string;controlDir:string;toolsHome?:string;sharedWorkspace?:string;additionalWorkspaces?:string[];model?:string;effort?:string;prompt:string;codexSandbox?:'external';codexProvider?:CodexProviderBinding}
11
+ type Options = {workspace:string;controlDir:string;toolsHome?:string;sharedWorkspace?:string;additionalWorkspaces?:string[];model?:string;effort?:string;prompt:string;codexSandbox?:'external';codexProvider?:CodexProviderBinding;telemetry?:boolean}
11
12
  type Message = {id?:number;method?:string;params?:any;result?:any;error?:{message:string;code?:number}}
12
13
 
13
14
  // Keep Codex's native session alive. Codex itself starts goal continuation turns;
@@ -19,6 +20,15 @@ export async function runCodexSession(options:Options, io:{launch?:()=>ChildProc
19
20
  const child=io.launch?.() ?? spawn('codex',['app-server','--stdio','--disable','memories','--enable','skip_host_skill_discovery'],{cwd:options.workspace,env:process.env,stdio:['pipe','pipe','pipe']})
20
21
  const emit=io.emit ?? (line=>process.stdout.write(line+'\n'))
21
22
  let id=0,threadId:string|undefined,activeTurn:string|undefined,finished=false,sawTurn=false,hadGoal=false,workBegan=false,resetAt:string|undefined
23
+ // Owner-enabled telemetry: app-server items and usage in the exec event shape.
24
+ const telemetry=options.telemetry ? telemetryObserver() : undefined
25
+ let reported=''
26
+ const observe=(event?:object)=>{
27
+ if(!telemetry)return
28
+ if(event)telemetry.write(JSON.stringify(event)+'\n')
29
+ const line=telemetry.summary();if(line!==reported){reported=line;emit(line)}
30
+ }
31
+ const tools:Record<string,string>={commandExecution:'command_execution',fileChange:'file_change',webSearch:'web_search',mcpToolCall:'mcp_tool_call'}
22
32
  let resolveDone!:(code:number)=>void
23
33
  const done=new Promise<number>(resolve=>{resolveDone=resolve})
24
34
  const pending=new Map<number,{resolve:(value:any)=>void;reject:(error:Error)=>void;timer:ReturnType<typeof setTimeout>}>()
@@ -33,6 +43,7 @@ export async function runCodexSession(options:Options, io:{launch?:()=>ChildProc
33
43
  const settled=(goal:any)=>{
34
44
  if(goal)hadGoal=true
35
45
  if(activeTurn || goal?.status==='active')return
46
+ if(goal?.status==='usageLimited')emit(startupLine({workBegan,rejection:{category:'quota',...(resetAt?{resetAt}:{})}}))
36
47
  if(goal && goal.status!=='complete'){console.error(`Native goal stopped: ${goal.status}`);finish(1);return}
37
48
  if(!goal && hadGoal){fail(new Error('Native goal disappeared without verified completion'));return}
38
49
  if(!sawTurn)return
@@ -64,6 +75,11 @@ export async function runCodexSession(options:Options, io:{launch?:()=>ChildProc
64
75
  if(message.method==='account/rateLimits/updated')resetAt=codexResetAt(message.params?.rateLimits) ?? resetAt
65
76
  if(message.params?.threadId!==threadId)return
66
77
  if(message.method==='turn/started'){activeTurn=message.params.turn.id;sawTurn=true}
78
+ const item=message.params?.item
79
+ if((message.method==='item/started'||message.method==='item/completed') && tools[item?.type])
80
+ observe({type:message.method==='item/started'?'item.started':'item.completed',item:{id:item.id,type:tools[item.type],server:item.server,tool:item.tool,status:['failed','declined'].includes(item.status)?'failed':'completed'}})
81
+ if(message.method==='thread/tokenUsage/updated'){const total=message.params.tokenUsage?.total
82
+ observe({type:'turn.completed',usage:{input_tokens:total?.inputTokens,cached_input_tokens:total?.cachedInputTokens,output_tokens:total?.outputTokens}})}
67
83
  // Any item beyond the echoed input means the provider accepted the turn.
68
84
  if(message.method==='item/started' && message.params.item?.type!=='userMessage' && !workBegan){workBegan=true;emit(startupLine({workBegan}))}
69
85
  if(message.method==='thread/goal/updated')settled(message.params.goal)
@@ -73,7 +89,9 @@ export async function runCodexSession(options:Options, io:{launch?:()=>ChildProc
73
89
  if(message.params.turn.status!=='completed'){
74
90
  if(message.params.turn.status==='interrupted')finish(130)
75
91
  else {
76
- const category=workBegan ? undefined : codexRejection(message.params.turn.error?.codexErrorInfo)
92
+ observe({type:'turn.failed'})
93
+ const rejected=codexRejection(message.params.turn.error?.codexErrorInfo)
94
+ const category=!workBegan || rejected==='quota' ? rejected : undefined
77
95
  if(category)emit(startupLine({workBegan,rejection:{category,...(category==='quota' && resetAt ? {resetAt} : {})}}))
78
96
  fail(new Error(redactFailure(typeof message.params.turn.error?.message==='string'
79
97
  ? message.params.turn.error.message : 'Native turn failed without error details')))
@@ -88,6 +106,8 @@ export async function runCodexSession(options:Options, io:{launch?:()=>ChildProc
88
106
  try{
89
107
  await request('initialize',{clientInfo:{name:'ezenciel-agents',version:'1'},capabilities:{experimentalApi:true}})
90
108
  send({method:'initialized',params:{}})
109
+ // Read native quota metadata even when a rejected turn emits no update.
110
+ await request('account/rateLimits/read',{}).then(limits=>{resetAt=codexResetAt(limits.rateLimits)}).catch(()=>{})
91
111
  const result=await request('thread/start',{
92
112
  cwd:options.workspace,approvalPolicy:'never',sandbox:options.codexSandbox === 'external' ? 'danger-full-access' : 'workspace-write',model:options.model,
93
113
  config:{project_root_markers:['AGENTS.md','.git'],'sandbox_workspace_write.writable_roots':[options.controlDir,...(options.toolsHome?[options.toolsHome]:[]),...(options.sharedWorkspace?[options.sharedWorkspace]:[]),...(options.additionalWorkspaces ?? [])],
@@ -96,6 +116,7 @@ export async function runCodexSession(options:Options, io:{launch?:()=>ChildProc
96
116
  })
97
117
  threadId=result.thread?.id
98
118
  if(!threadId)throw new Error('Codex did not return a native thread ID')
119
+ telemetry?.model(result.model)
99
120
  emit(JSON.stringify({type:'thread.started',thread_id:threadId}))
100
121
  await request('turn/start',{threadId,input:[{type:'text',text:options.prompt}],model:options.model,effort:options.effort,
101
122
  ...(options.codexSandbox === 'external' ? {sandboxPolicy:{type:'externalSandbox',networkAccess:'enabled'}} : {})})