ennodia 0.5.1 → 0.5.3
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/README.md +2 -2
- package/docs/getting-started.md +3 -0
- package/docs/install.md +3 -1
- package/docs/reference/mcp-tools.md +15 -5
- package/docs/reference/supported-harnesses.md +10 -8
- package/package.json +1 -1
- package/src/core.ts +3 -16
- package/src/diagnosis.ts +3 -1
- package/src/harnesses.ts +23 -1
- package/src/installation.ts +4 -2
- package/src/readiness.ts +15 -12
- package/src/server.ts +9 -9
- package/src/setup-app.ts +10 -10
- package/src/tasks.ts +22 -3
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ See [how to read these notes](https://ennodia.cherninlab.com/docs/guides/underst
|
|
|
85
85
|
|
|
86
86
|
Codex CLI, Claude Code, OpenCode, Kilo Code, Kiro CLI, Cline, Hermes Agent, and Antigravity.
|
|
87
87
|
Model availability and tool permissions depend on the installed agent and its provider configuration.
|
|
88
|
-
Discovery reports native login checks and recovery actions.
|
|
88
|
+
Discovery reports native login checks and confirmed recovery actions. The requested task verifies model access without a separate model call first.
|
|
89
89
|
Codex workers use a read-only sandbox by default.
|
|
90
90
|
|
|
91
91
|
Ennodia discovers native Agent Skills and can request them for specific tasks.
|
|
@@ -142,7 +142,7 @@ Savings are not yet validated.
|
|
|
142
142
|
Ennodia checks selected workers, Judge, and Advisor routes before execution.
|
|
143
143
|
Blocked routes return a native recovery action before reviewers start.
|
|
144
144
|
Ennodia says nothing about its own status unless an agent you asked for is blocked.
|
|
145
|
-
Rechecks
|
|
145
|
+
Rechecks and blocked starts return compact diagnostics. The full setup panel appears only for an explicit setup request.
|
|
146
146
|
|
|
147
147
|
Your agent calls `ennodia_setup` to show each agent's status and fix in your current session.
|
|
148
148
|
Hosts that render MCP Apps, such as Claude Desktop and Visual Studio Code, can show it as a card with check buttons.
|
package/docs/getting-started.md
CHANGED
|
@@ -34,6 +34,9 @@ Start with one worker and a small set of files.
|
|
|
34
34
|
Ennodia checks each selected agent before execution. Your agent hears nothing about Ennodia's status unless an agent you requested is blocked.
|
|
35
35
|
A blocked agent comes with its exact fix, such as a sign-in command.
|
|
36
36
|
|
|
37
|
+
The requested task verifies model access. Ennodia does not add a separate model call before it.
|
|
38
|
+
An optional verification timeout leaves access unverified and does not prevent a task from starting.
|
|
39
|
+
|
|
37
40
|
For setup or recovery, give your agent this request:
|
|
38
41
|
|
|
39
42
|
```text
|
package/docs/install.md
CHANGED
|
@@ -81,7 +81,9 @@ Tell the user which step is needed before trying to call Ennodia.
|
|
|
81
81
|
|
|
82
82
|
Ennodia includes an installation self-check in run results and rechecks. Its MCP startup instructions carry only installation problems. It uses the loaded code location to identify a source checkout, project-local dependency, global installation, package cache, or unknown scope. It checks the Bun version, package version, core files, bundled skills, and filesystem access for temporary workspaces, history, and harness update receipts. Development files are not required for a packaged install. The npm launcher stops before startup if Bun is too old.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Report confirmed installation blockers briefly. A running process using an older version needs a reconnect. This does not mean the installation is broken. Do not repeat unchanged warnings.
|
|
85
|
+
|
|
86
|
+
Call `ennodia_check_harnesses` with `refresh: true` to refresh the installation report and native harness status after a repair. Filesystem access checks do not prove free disk space or future writes. They do not verify provider access or the latest published package version. A global install gives no extra permissions. The user's normal permissions still apply.
|
|
85
87
|
|
|
86
88
|
The report also identifies compiled code. A compiled executable does not need separate source entrypoints. Missing bundled skills remain a reported limitation. Compilation is currently an experimental build path.
|
|
87
89
|
|
|
@@ -733,7 +733,7 @@ pass that task ID as `continueTaskId` in the next run. The follow-up keeps the
|
|
|
733
733
|
harness and directory. It inherits the model and reasoning effort unless you
|
|
734
734
|
supply new values. Each follow-up has a new task ID and execution allowance.
|
|
735
735
|
|
|
736
|
-
|
|
736
|
+
Native status checks use the inherited or explicitly supplied model before the follow-up starts.
|
|
737
737
|
An earlier turn cannot be used once a follow-up has started.
|
|
738
738
|
|
|
739
739
|
Continuation requires a task still held by the same running Ennodia server.
|
|
@@ -813,7 +813,7 @@ Sign-in and credential changes stay in each agent's own CLI.
|
|
|
813
813
|
|
|
814
814
|
#### Setup card
|
|
815
815
|
|
|
816
|
-
`ennodia_setup`
|
|
816
|
+
Only `ennodia_setup` names the `ui://ennodia/setup.html` resource in `_meta.ui.resourceUri`.
|
|
817
817
|
The resource uses the MCP Apps type `text/html;profile=mcp-app`.
|
|
818
818
|
Hosts that render MCP Apps show the card inside the conversation. Other hosts ignore it and keep the Markdown panel.
|
|
819
819
|
|
|
@@ -837,15 +837,25 @@ The result includes `installation`, a local self-check of Ennodia's loaded code
|
|
|
837
837
|
It accepts `harnessIds`, `model`, `cwd`, `refresh`, `verify`, and `upgrade`.
|
|
838
838
|
Native status checks are the default. `verify: true` makes a bounded real model request.
|
|
839
839
|
|
|
840
|
-
`unverified` means
|
|
840
|
+
`unverified` means model access is unknown. Normal runs use native status checks and let the task verify model access.
|
|
841
|
+
An inconclusive check does not block the task or mark the agent as needing setup.
|
|
841
842
|
Use targeted `harnessIds` for recovery checks. Unrelated blockers do not prevent work on usable requested routes.
|
|
842
843
|
|
|
843
|
-
|
|
844
|
+
Only `ennodia_setup` includes the embedded `ennodia://setup` resource with `text/markdown` content.
|
|
845
|
+
Rechecks and blocked preflight responses return compact JSON diagnostics without creating another setup card.
|
|
844
846
|
The JSON text response keeps its existing shape.
|
|
845
847
|
|
|
846
848
|
The panel is a table of agents, with agents that need the user first. Each row gives the status in plain words and the next step.
|
|
847
849
|
Sign-in commands follow the table, for login blockers only. Key expiry and quota blockers name their own recovery.
|
|
848
|
-
|
|
850
|
+
|
|
851
|
+
Show the full panel only for an explicit setup or status request.
|
|
852
|
+
For a blocked task, report its cause and native recovery action briefly.
|
|
853
|
+
Do not repeat unchanged warnings or automatically retry checks.
|
|
854
|
+
Recheck the same harness and model after recovery, then resume the original task.
|
|
855
|
+
|
|
856
|
+
When the installed package differs from the running process, `installation.restartRequired` is true.
|
|
857
|
+
This is a reload notice, not a broken installation. Reconnect the MCP connection to load the installed version.
|
|
858
|
+
Missing runtime files or an unsupported runtime still report a broken installation.
|
|
849
859
|
|
|
850
860
|
`upgrade: true` runs supported updaters before rediscovery and recheck.
|
|
851
861
|
This tool can change installed software and call providers, so it does not declare `readOnlyHint`.
|
|
@@ -45,12 +45,14 @@ It does not guarantee media support, authentication, or access to a requested mo
|
|
|
45
45
|
Plan Advisor receives this guidance in its harness inventory.
|
|
46
46
|
|
|
47
47
|
Discovery checks installation and native login status through supported commands.
|
|
48
|
-
Model access remains unverified until
|
|
49
|
-
A stalled check stops at its deadline and
|
|
48
|
+
Model access remains unverified until an explicit verification request or a successful task returns an answer.
|
|
49
|
+
A stalled check stops at its deadline and leaves access unverified.
|
|
50
50
|
Ennodia checks selected worker, Judge, and Advisor routes before execution.
|
|
51
51
|
Known blockers prevent requested work from starting.
|
|
52
52
|
Automatic routing excludes blocked routes and reports missing coverage.
|
|
53
53
|
|
|
54
|
+
Normal runs check native status without making a separate model request.
|
|
55
|
+
|
|
54
56
|
## Media inputs
|
|
55
57
|
|
|
56
58
|
Ennodia sends a text prompt through the selected CLI. Include local file paths and requested ranges in that prompt.
|
|
@@ -138,7 +140,7 @@ Call `ennodia_check_harnesses` after sign-in:
|
|
|
138
140
|
```
|
|
139
141
|
|
|
140
142
|
Pass the same `model` and `cwd` when rechecking an explicit route.
|
|
141
|
-
The check returns `actionRequired` for login, setup, model,
|
|
143
|
+
The check returns `actionRequired` for confirmed login, setup, model, and capacity blockers.
|
|
142
144
|
A missing CLI requires installation. A capacity failure requires capacity recovery.
|
|
143
145
|
Neither is fixed by repeated sign-in.
|
|
144
146
|
|
|
@@ -157,8 +159,8 @@ Native checks cache for 30 seconds. Verified routes cache for five minutes.
|
|
|
157
159
|
A native authentication failure invalidates the route immediately.
|
|
158
160
|
Concurrent checks of the same route share one request.
|
|
159
161
|
|
|
160
|
-
`unverified` means
|
|
161
|
-
`ennodia_run`
|
|
162
|
+
`unverified` means model access is unknown. It does not mean a failed harness.
|
|
163
|
+
`ennodia_run` lets the requested task verify model access after native status checks.
|
|
162
164
|
Checking every supported harness is not a prerequisite for a run.
|
|
163
165
|
|
|
164
166
|
Unrelated harness blockers do not prevent work on a usable requested route.
|
|
@@ -169,9 +171,9 @@ Ennodia does not require an additional approval step.
|
|
|
169
171
|
Model probes run in an empty temporary directory with normal native permissions.
|
|
170
172
|
A temporary directory is not a security sandbox.
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
Explicit verification uses provider capacity independently of a run's budget.
|
|
175
|
+
Normal runs add no model probe calls or input-token reserves.
|
|
176
|
+
A verification timeout after 25 seconds does not block the requested task or imply a setup problem.
|
|
175
177
|
|
|
176
178
|
Before ordinary execution, Ennodia attempts supported upgrades once per day while no Ennodia task is active.
|
|
177
179
|
Homebrew installations use their installed formula or cask.
|
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -416,7 +416,7 @@ export class EnnodiaCore {
|
|
|
416
416
|
for (const h of harnesses) { const fresh = refreshed.find(f => f.id === h.id); if (fresh) Object.assign(h, fresh); }
|
|
417
417
|
}
|
|
418
418
|
const unique = [...new Map(routes.map(r => [JSON.stringify(r), r])).values()];
|
|
419
|
-
//
|
|
419
|
+
// Native checks catch known blockers; the requested task verifies model access.
|
|
420
420
|
const checked = await Promise.all(unique.map(async route => {
|
|
421
421
|
const h = harnesses.find(h => h.id === route.harnessId);
|
|
422
422
|
if (!h) throw new Error(`Unknown harness: ${route.harnessId}`);
|
|
@@ -425,23 +425,10 @@ export class EnnodiaCore {
|
|
|
425
425
|
const initialBlocked = checked.filter(c => readinessBlocked(c.readiness)).map(c => ({ harnessId: c.h.id, harnessName: c.h.name, readiness: c.readiness }));
|
|
426
426
|
if (initialBlocked.length) throw new HarnessReadinessError(initialBlocked);
|
|
427
427
|
for (const { h, readiness } of checked) {
|
|
428
|
-
if (readiness.state !== "ready") {
|
|
429
|
-
// Probes are real provider calls. Reserve them before starting, and
|
|
430
|
-
// expose their estimate separately rather than implying free checks.
|
|
431
|
-
report.modelProbes++; report.estimatedInputTokens += 256;
|
|
432
|
-
}
|
|
433
428
|
report.checks.push({ harnessId: h.id, harnessName: h.name, readiness });
|
|
434
|
-
|
|
435
|
-
if (budget?.maxEstimatedInputTokens !== undefined && report.estimatedInputTokens > budget.maxEstimatedInputTokens) throw new Error("Budget limit exceeded: readiness probe input reserve.");
|
|
436
|
-
}
|
|
437
|
-
const verified = await Promise.all(report.checks.map(async c => ({ ...c, readiness: await this.readinessManager!.check(harnesses.find(h => h.id === c.harnessId)!, this.findHarnessAdapter(c.harnessId), { model: c.readiness.model, cwd, verify: true }) })));
|
|
438
|
-
report.checks = verified;
|
|
439
|
-
const blocked = verified.filter(c => readinessBlocked(c.readiness));
|
|
440
|
-
if (blocked.length) throw new HarnessReadinessError(blocked);
|
|
441
|
-
for (const check of verified) {
|
|
442
|
-
const h = harnesses.find(h => h.id === check.harnessId);
|
|
443
|
-
if (h) h.readiness = check.readiness;
|
|
429
|
+
h.readiness = readiness;
|
|
444
430
|
}
|
|
431
|
+
if (budget?.maxChildTasks !== undefined && routes.length > budget.maxChildTasks) throw new Error("Budget limit exceeded: selected routes exceed maxChildTasks.");
|
|
445
432
|
return report;
|
|
446
433
|
}
|
|
447
434
|
|
package/src/diagnosis.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TaskView } from "./tasks";
|
|
2
2
|
import { truncate } from "./internal";
|
|
3
|
+
import { openCodeQuotaError } from "./harnesses";
|
|
3
4
|
|
|
4
5
|
export type TaskOutputPreview = {
|
|
5
6
|
harnessId: string;
|
|
@@ -150,7 +151,8 @@ export function hasCodexQuotaError(stdout: string): boolean {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
function hasQuotaError(task: TaskView): boolean {
|
|
153
|
-
return task.harnessId === "
|
|
154
|
+
return task.harnessId === "opencode" ? Boolean(openCodeQuotaError(task.stderr))
|
|
155
|
+
: task.harnessId === "codex" && (hasCodexQuotaError(task.stdout) || hasCodexQuotaError(task.stderr));
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
function hasAuthenticationError(task: TaskView): boolean {
|
package/src/harnesses.ts
CHANGED
|
@@ -95,6 +95,8 @@ export type HarnessAdapter = {
|
|
|
95
95
|
extractSessionId?: (stdout: string) => string | undefined;
|
|
96
96
|
/** Some public CLIs report a semantic failure with exit code zero. */
|
|
97
97
|
failureReason?: (stdout: string, stderr: string) => string | undefined;
|
|
98
|
+
/** A confirmed native failure that must stop a still-running CLI. */
|
|
99
|
+
streamFailureReason?: (stdout: string, stderr: string) => string | undefined;
|
|
98
100
|
/** The CLI accepts operator instructions separately from the prompt. A
|
|
99
101
|
* worker then reads Ennodia's limits as its session's own, not as text
|
|
100
102
|
* inside the task, which a careful model can distrust. */
|
|
@@ -312,8 +314,10 @@ export const harnessAdapters: HarnessAdapter[] = [
|
|
|
312
314
|
notes: ["Runs through `opencode run` without permission-bypass flags."],
|
|
313
315
|
// Concurrent starts can fail while another instance holds its database.
|
|
314
316
|
startupConflict: /database is locked/i,
|
|
317
|
+
streamFailureReason: (_stdout, stderr) => openCodeQuotaError(stderr),
|
|
315
318
|
buildCommand: (commandPath, input) => {
|
|
316
|
-
|
|
319
|
+
// Retryable provider errors otherwise stay in OpenCode's private log.
|
|
320
|
+
const args = ["run", "--print-logs", "--log-level", "ERROR"];
|
|
317
321
|
|
|
318
322
|
if (input.cwd) {
|
|
319
323
|
args.push("--dir", input.cwd);
|
|
@@ -501,6 +505,24 @@ export const harnessAdapters: HarnessAdapter[] = [
|
|
|
501
505
|
},
|
|
502
506
|
];
|
|
503
507
|
|
|
508
|
+
/** OpenCode's public --print-logs stderr; never inspect worker stdout here. */
|
|
509
|
+
export function openCodeQuotaError(stderr: string): string | undefined {
|
|
510
|
+
for (const line of stderr.split(/\r?\n/)) {
|
|
511
|
+
if (!/^timestamp=\S+ level=ERROR /.test(line) ||
|
|
512
|
+
!/ message="stream error" /.test(line) ||
|
|
513
|
+
!/ small=false /.test(line) || !/ mode=primary /.test(line)) continue;
|
|
514
|
+
// Ignore small/title requests: they can fail while the main agent works.
|
|
515
|
+
const encoded = / error\.error=("(?:[^"\\]|\\.)*")(?= |$)/.exec(line)?.[1];
|
|
516
|
+
if (!encoded) continue;
|
|
517
|
+
let message: string;
|
|
518
|
+
try { message = JSON.parse(encoded); } catch { continue; }
|
|
519
|
+
if (/^AI_APICallError:\s*(?:OpenCode )?Go usage limit (?:reached|exceeded)\b/i.test(message)) {
|
|
520
|
+
return message.replace(/^AI_APICallError:\s*/i, "");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
|
|
504
526
|
function extractCodexSessionId(stdout: string): string | undefined {
|
|
505
527
|
for (const line of stdout.split(/\r?\n/)) {
|
|
506
528
|
const event = parseJsonObject(line);
|
package/src/installation.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type InstallationReport = {
|
|
|
13
13
|
status: "healthy" | "limited" | "broken";
|
|
14
14
|
version: string;
|
|
15
15
|
packageVersion?: string;
|
|
16
|
+
restartRequired?: boolean;
|
|
16
17
|
distribution: "source" | "compiled";
|
|
17
18
|
scope: InstallationScope;
|
|
18
19
|
evidence: string;
|
|
@@ -59,7 +60,8 @@ export function inspectInstallation(options: InstallationOptions = {}): Installa
|
|
|
59
60
|
const jsr = readMetadata(join(codeRoot, "jsr.json"));
|
|
60
61
|
const metadata = npm?.name === "ennodia" ? npm : jsr?.name === "@cherninlab/ennodia" ? jsr : undefined;
|
|
61
62
|
const packageVersion = typeof metadata?.version === "string" ? metadata.version : undefined;
|
|
62
|
-
|
|
63
|
+
const restartRequired = Boolean(packageVersion && packageVersion !== ENNODIA_VERSION);
|
|
64
|
+
add("package-version", packageVersion === ENNODIA_VERSION ? "pass" : "warning", restartRequired ? `Installed ${packageVersion}; running ${ENNODIA_VERSION}. Reconnect the Ennodia MCP connection to load the installed version.` : packageVersion ? `Metadata ${packageVersion}; loaded code ${ENNODIA_VERSION}.` : "Package metadata is unavailable; only the loaded code version is known.");
|
|
63
65
|
|
|
64
66
|
// JSR does not ship the npm launcher. Development tooling is not required.
|
|
65
67
|
const declaredModules = Array.isArray(npm?.files) ? npm.files.filter((p): p is string => typeof p === "string" && /^src\/[^/*]+\.ts$/.test(p)) : [];
|
|
@@ -99,7 +101,7 @@ export function inspectInstallation(options: InstallationOptions = {}): Installa
|
|
|
99
101
|
const launcher = launcherPath && isAbsolute(launcherPath) && canonical(launcherPath) === canonical(join(codeRoot, "bin/ennodia")) ? launcherPath : undefined;
|
|
100
102
|
return {
|
|
101
103
|
checkedAt: new Date().toISOString(), status: checks.some(c => c.status === "fail") ? "broken" : limitations.length ? "limited" : "healthy",
|
|
102
|
-
version: ENNODIA_VERSION, packageVersion, distribution: compiled ? "compiled" : "source", scope, evidence, codeRoot, serverCwd, launcher,
|
|
104
|
+
version: ENNODIA_VERSION, packageVersion, ...(restartRequired ? { restartRequired } : {}), distribution: compiled ? "compiled" : "source", scope, evidence, codeRoot, serverCwd, launcher,
|
|
103
105
|
runtime: { name: bunVersion ? "bun" : "unsupported", version: bunVersion, executable: process.execPath, minimumVersion: MIN_BUN_VERSION, platform: process.platform, arch: process.arch },
|
|
104
106
|
checks, storage: { temporaryDirectory, history, harnessUpdates: updates }, limitations,
|
|
105
107
|
};
|
package/src/readiness.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { stripVTControlCharacters } from "node:util";
|
|
6
6
|
import type { CommandSpec, HarnessAdapter, HarnessDiscovery, HarnessRunInput } from "./harnesses";
|
|
7
|
+
import { openCodeQuotaError } from "./harnesses";
|
|
7
8
|
import { signalOwnedProcess } from "./process";
|
|
8
9
|
|
|
9
10
|
export type ReadinessState = "unverified" | "ready" | "login-required" | "credentials-expired" | "setup-required" | "model-unavailable" | "quota-exhausted" | "unreachable" | "updating";
|
|
@@ -28,7 +29,7 @@ export type HarnessReadinessOptions = {
|
|
|
28
29
|
isBusy?: () => boolean;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
const BLOCKED = new Set<ReadinessState>(["login-required", "credentials-expired", "setup-required", "model-unavailable", "quota-exhausted", "
|
|
32
|
+
const BLOCKED = new Set<ReadinessState>(["login-required", "credentials-expired", "setup-required", "model-unavailable", "quota-exhausted", "updating"]);
|
|
32
33
|
export function readinessBlocked(readiness?: HarnessReadiness): boolean { return Boolean(readiness && BLOCKED.has(readiness.state)); }
|
|
33
34
|
export function harnessAttention(harnesses: HarnessDiscovery[]): HarnessAttention[] {
|
|
34
35
|
return harnesses.filter(h => readinessBlocked(h.readiness)).map(h => ({ harnessId: h.id, harnessName: h.name, readiness: h.readiness! }));
|
|
@@ -42,6 +43,7 @@ export class HarnessReadinessError extends Error {
|
|
|
42
43
|
|
|
43
44
|
/** Recognize native failure envelopes, never quoted prose inside an answer. */
|
|
44
45
|
export function nativeReadinessFailure(stdout: string, stderr: string, allowPlainStdout = false): ReadinessState | undefined {
|
|
46
|
+
if (openCodeQuotaError(stderr)) return "quota-exhausted";
|
|
45
47
|
const stderrLines = stripVTControlCharacters(stderr).split(/\r?\n/);
|
|
46
48
|
const lines = [...stderrLines, ...stripVTControlCharacters(stdout).split(/\r?\n/)];
|
|
47
49
|
for (const [index, line] of lines.entries()) {
|
|
@@ -58,7 +60,7 @@ export function nativeReadinessFailure(stdout: string, stderr: string, allowPlai
|
|
|
58
60
|
message = stripVTControlCharacters(message).trim();
|
|
59
61
|
if (/^(?:Error:\s*)?(?:This |The |Your )?API key (?:has expired|is expired)\b/i.test(message)) return "credentials-expired";
|
|
60
62
|
if (/^(?:(?:API )?Error:\s*)?(?:Failed to authenticate\b|OAuth session expired\b|Authentication failed\b|Not logged in\b|Unauthorized\b|401\b|Re-authentication required\b|No access token found\b|Token refresh failed\b|Failed to refresh (?:OAuth|token)\b|Your authentication token .*expired\b)/i.test(message)) return "login-required";
|
|
61
|
-
if (/^(?:Error:\s*)?(?:You've hit your usage limit\b|quota (?:exceeded|exhausted)\b|insufficient (?:credits|balance)\b|rate limit exceeded\b|Add credits to continue\b)/i.test(message)) return "quota-exhausted";
|
|
63
|
+
if (/^(?:Error:\s*)?(?:You've hit your (?:usage|\d+[ -]hour|weekly) limit\b|quota (?:exceeded|exhausted)\b|insufficient (?:credits|balance)\b|rate limit exceeded\b|Add credits to continue\b)/i.test(message)) return "quota-exhausted";
|
|
62
64
|
if (/^(?:Error:\s*)?(?:model .* (?:not found|not available|not supported)|unknown model\b|ModelNotFoundError\b)/i.test(message)) return "model-unavailable";
|
|
63
65
|
if (/^(?:Error:\s*)?(?:No (?:API key|provider|credentials)\b|Missing .*API.?KEY\b|unrecognized arguments\b|unknown (?:option|command)\b)/i.test(message)) return "setup-required";
|
|
64
66
|
}
|
|
@@ -98,7 +100,7 @@ export function harnessUpgradeCommand(h: HarnessDiscovery): string[] | undefined
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
const messages: Record<ReadinessState, string> = {
|
|
101
|
-
unverified: "Model access is unverified
|
|
103
|
+
unverified: "Model access is unverified. A task can run without a separate model check.",
|
|
102
104
|
ready: "The selected route returned a verified response.",
|
|
103
105
|
"login-required": "Sign-in is required before review.",
|
|
104
106
|
"credentials-expired": "The configured provider API key has expired. Replace the key in the native CLI/provider settings; signing in again will not renew it.",
|
|
@@ -137,10 +139,9 @@ export class HarnessReadinessManager {
|
|
|
137
139
|
private value(h: HarnessDiscovery, state: ReadinessState, model?: string): HarnessReadiness {
|
|
138
140
|
const r = recipe(h.id);
|
|
139
141
|
const result: HarnessReadiness = { state, checkedAt: new Date(this.now()).toISOString(), model, message: messages[state], upgrade: this.updates.get(h.id) };
|
|
140
|
-
if (state !== "ready") result.recovery = {
|
|
141
|
-
command: h.commandPath ? [h.commandPath, ...(state === "login-required" || state === "credentials-expired"
|
|
142
|
+
if (state !== "ready" && state !== "unverified") result.recovery = {
|
|
143
|
+
command: h.commandPath ? [h.commandPath, ...(state === "login-required" || state === "credentials-expired" ? r.login : ["--help"])] : [],
|
|
142
144
|
instruction: !h.commandPath ? "Install the supported native CLI, then recheck."
|
|
143
|
-
: state === "unverified" ? "Run the targeted recheck to verify model access. Sign in only when a login blocker is reported."
|
|
144
145
|
: state === "login-required" ? "Run the native command in an interactive terminal using the intended account/provider, then recheck."
|
|
145
146
|
: state === "credentials-expired" ? "Replace the expired API key through the native credential flow for the intended provider, then recheck the same route. Signing in again will not renew the key."
|
|
146
147
|
: state === "quota-exhausted" ? "Restore credits or quota through the intended provider, then recheck the same route. Do not repeat sign-in or switch to a different model silently."
|
|
@@ -181,7 +182,7 @@ export class HarnessReadinessManager {
|
|
|
181
182
|
const flightKey = `${key}:${Boolean(input.verify)}`;
|
|
182
183
|
const flight = this.flights.get(flightKey);
|
|
183
184
|
if (flight) return flight;
|
|
184
|
-
const pending = this.performCheck(h, adapter, input).catch(() => this.value(h, "
|
|
185
|
+
const pending = this.performCheck(h, adapter, input).catch(() => ({ ...this.value(h, "unverified", input.model), message: "The native check could not complete. Model access remains unverified; a task can still run." })).then(value => {
|
|
185
186
|
this.cache.set(key, { at: this.now(), value });
|
|
186
187
|
return value;
|
|
187
188
|
}).finally(() => this.flights.delete(flightKey));
|
|
@@ -201,8 +202,8 @@ export class HarnessReadinessManager {
|
|
|
201
202
|
if (h.id === "claude-code") {
|
|
202
203
|
try { if (JSON.parse(result.stdout).loggedIn === false) return this.value(h, "login-required", input.model); } catch { /* verification below */ }
|
|
203
204
|
}
|
|
204
|
-
if (result.timedOut) return this.value(h, "
|
|
205
|
-
if (result.exitCode !== 0) return this.value(h,
|
|
205
|
+
if (result.timedOut) return { ...this.value(h, "unverified", input.model), message: "The native sign-in check timed out. No sign-in or setup failure was confirmed." };
|
|
206
|
+
if (result.exitCode !== 0) return { ...this.value(h, "unverified", input.model), message: "The native check exited without a recognized blocker. A task can still run." };
|
|
206
207
|
}
|
|
207
208
|
if (!input.verify) return this.value(h, "unverified", input.model);
|
|
208
209
|
const dir = mkdtempSync(join(tmpdir(), "ennodia-readiness-"));
|
|
@@ -212,11 +213,12 @@ export class HarnessReadinessManager {
|
|
|
212
213
|
const result = await this.runner(spec, 25_000);
|
|
213
214
|
const failure = nativeReadinessFailure(result.stdout, result.stderr, true);
|
|
214
215
|
if (failure) return this.value(h, failure, input.model);
|
|
215
|
-
if (result.timedOut
|
|
216
|
+
if (result.timedOut) return { ...this.value(h, "unverified", input.model), message: "Model verification timed out after 25 seconds. A task can still run with its own time limit." };
|
|
217
|
+
if (result.exitCode !== 0 || adapter.failureReason?.(result.stdout, result.stderr)) return { ...this.value(h, "unverified", input.model), message: "Model verification did not complete. A task can still run." };
|
|
216
218
|
let answer = adapter.readinessAnswer ? adapter.readinessAnswer(result.stdout) ?? "" : result.stdout;
|
|
217
219
|
try { answer = readFileSync(join(dir, "answer.txt"), "utf8"); } catch { /* native text output */ }
|
|
218
220
|
// An echoed prompt is not success. Require a standalone returned nonce.
|
|
219
|
-
if (!stripVTControlCharacters(answer).split(/\r?\n/).some(line => line.trim() === nonce)) return this.value(h, "
|
|
221
|
+
if (!stripVTControlCharacters(answer).split(/\r?\n/).some(line => line.trim() === nonce)) return { ...this.value(h, "unverified", input.model), message: "Model verification did not return the expected response. A task can still run." };
|
|
220
222
|
return this.value(h, "ready", input.model);
|
|
221
223
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
222
224
|
}
|
|
@@ -242,7 +244,8 @@ export class HarnessReadinessManager {
|
|
|
242
244
|
const command = harnessUpgradeCommand(h);
|
|
243
245
|
const checkedAt = new Date(this.now()).toISOString();
|
|
244
246
|
const save = (status: NonNullable<HarnessReadiness["upgrade"]>["status"], message: string) => {
|
|
245
|
-
this.updates.set(h.id, { status, checkedAt, command, message });
|
|
247
|
+
this.updates.set(h.id, { status, checkedAt, command, message });
|
|
248
|
+
if (status === "succeeded" || status === "failed") this.invalidate(h.id);
|
|
246
249
|
};
|
|
247
250
|
if (!command) { save("unsupported", "No supported updater was found for this installation. Use its original installer."); return; }
|
|
248
251
|
if (this.options.isBusy?.() || this.flights.size > 0) { save("deferred", "Upgrade deferred while Ennodia work is active."); return; }
|
package/src/server.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function createEnnodiaServer(
|
|
|
108
108
|
catch (error) {
|
|
109
109
|
if (error instanceof HarnessReadinessError) {
|
|
110
110
|
const installation = core.getInstallation?.();
|
|
111
|
-
return { ...
|
|
111
|
+
return { ...jsonResult({ error: error.message, installation, actionRequired: error.actionRequired, coverage: { started: 0 } }), isError: true };
|
|
112
112
|
}
|
|
113
113
|
throw error;
|
|
114
114
|
}
|
|
@@ -149,8 +149,7 @@ export function createEnnodiaServer(
|
|
|
149
149
|
|
|
150
150
|
server.registerTool("ennodia_check_harnesses", {
|
|
151
151
|
title: "Recheck or upgrade Ennodia harnesses",
|
|
152
|
-
description: "Recheck
|
|
153
|
-
_meta: SETUP_APP_TOOL_META,
|
|
152
|
+
description: "Recheck selected harnesses and return compact diagnostics. Target affected harnessIds; unrelated blockers do not prevent work. verify makes an optional real model call; a timeout leaves access unverified and does not block tasks. upgrade runs supported updaters before rechecking. Never switch account/provider/model silently. Show a brief changed result; use ennodia_setup only when the user asks for the full setup view.",
|
|
154
153
|
inputSchema: {
|
|
155
154
|
harnessIds: z.array(z.string().min(1)).min(1).optional().describe("Harnesses to recheck. Omit for all installed harnesses."),
|
|
156
155
|
model: z.string().min(1).optional().describe("Exact model to verify on the selected route."),
|
|
@@ -161,7 +160,7 @@ export function createEnnodiaServer(
|
|
|
161
160
|
},
|
|
162
161
|
}, async input => {
|
|
163
162
|
const result = await core.checkHarnesses(input);
|
|
164
|
-
return
|
|
163
|
+
return jsonResult(result);
|
|
165
164
|
});
|
|
166
165
|
|
|
167
166
|
// Hosts that render MCP Apps show this card for the two tools above.
|
|
@@ -1337,9 +1336,9 @@ export async function shutdownEnnodia(
|
|
|
1337
1336
|
export function serverInstructions(installation?: InstallationReport): string {
|
|
1338
1337
|
const lines = [
|
|
1339
1338
|
"Ennodia hands work to the user's other local agents. Do not open with Ennodia's status or list agents the user did not ask about: ennodia_run checks the agents it selects before starting.",
|
|
1340
|
-
"
|
|
1339
|
+
"For a confirmed blocker on a requested agent, state its cause and native recovery action in one short line. Do not repeat unchanged warnings or automatically retry checks. After recovery, recheck that harness and model. If agents were skipped (actionRequired), say so in one line. Unrelated blockers must not hold up usable agents.",
|
|
1341
1340
|
"For setup or status requests, call ennodia_setup and show its panel. Hosts that render MCP Apps show it as a card; then add one line at most.",
|
|
1342
|
-
"Unverified means
|
|
1341
|
+
"Unverified means model access is unknown, not failed; the requested task can verify it. Use a separate model probe only when needed. A reload notice after an update is not a broken installation; mention it once when relevant. Never collect credentials in chat, switch account/provider/model silently, or claim a check ran when it did not. Follow existing authorization; add no approval step of your own.",
|
|
1343
1342
|
];
|
|
1344
1343
|
if (installation?.status === "broken") lines.push(`Ennodia's installation is broken: ${installation.limitations.join(" ")} Tell the user before starting work and link ${INSTALL_GUIDE_URL}.`);
|
|
1345
1344
|
else if (installation?.status === "limited") lines.push(`Installation notes, to mention only when they affect the request: ${installation.limitations.join(" ")}`);
|
|
@@ -1357,16 +1356,17 @@ function setupResult(value: unknown, routes: (HarnessDiscovery | HarnessAttentio
|
|
|
1357
1356
|
const cell = (text: string) => text.replaceAll("|", "\\|").replace(/[\r\n]+/g, " ");
|
|
1358
1357
|
const lines = ["## Ennodia agents"];
|
|
1359
1358
|
if (installation && installation.status !== "healthy") {
|
|
1360
|
-
lines.push("", `Installation ${installation.status}:`, ...installation.limitations.map(limit => `- ${limit}`)
|
|
1359
|
+
lines.push("", installation.status !== "broken" && installation.restartRequired ? "Ennodia update ready:" : `Installation ${installation.status}:`, ...installation.limitations.map(limit => `- ${limit}`));
|
|
1360
|
+
if (!installation.restartRequired || installation.status === "broken") lines.push("", `[Installation guide](${INSTALL_GUIDE_URL})`);
|
|
1361
1361
|
}
|
|
1362
1362
|
const rows = routes.map(route => {
|
|
1363
1363
|
const key = accessKey(route);
|
|
1364
|
-
return { name: "id" in route ? route.name : route.harnessName, model: route.readiness?.model, key, command: key === "login-required" ? route.readiness?.recovery?.command ?? [] : [] };
|
|
1364
|
+
return { name: "id" in route ? route.name : route.harnessName, model: route.readiness?.model, message: route.readiness?.message, key, command: key === "login-required" ? route.readiness?.recovery?.command ?? [] : [] };
|
|
1365
1365
|
}).sort((a, b) => accessRank(a.key) - accessRank(b.key));
|
|
1366
1366
|
if (rows.length) lines.push("", "| Agent | Status | Next step |", "| --- | --- | --- |");
|
|
1367
1367
|
else lines.push("", `No supported agents were found. Install one with the [harness guide](${HARNESS_GUIDE_URL}).`);
|
|
1368
1368
|
for (const row of rows) {
|
|
1369
|
-
const next = row.key === "missing" ? `${ACCESS_TEXT.missing.next} [Install guide](${HARNESS_GUIDE_URL})` : ACCESS_TEXT[row.key].next;
|
|
1369
|
+
const next = row.key === "missing" ? `${ACCESS_TEXT.missing.next} [Install guide](${HARNESS_GUIDE_URL})` : row.key === "unverified" && row.message ? row.message : ACCESS_TEXT[row.key].next;
|
|
1370
1370
|
lines.push(`| ${cell(row.name + (row.model ? ` · ${row.model}` : ""))} | ${ACCESS_TEXT[row.key].label} | ${cell(next)} |`);
|
|
1371
1371
|
}
|
|
1372
1372
|
// Only a login blocker calls for a command. Other blockers need credits or a new key.
|
package/src/setup-app.ts
CHANGED
|
@@ -21,16 +21,16 @@ export const ACCESS_TEXT: Record<AccessKey, { label: string; next: string }> = {
|
|
|
21
21
|
"quota-exhausted": { label: "Out of quota", next: "Restore credits or quota with the provider, then check again. Signing in does not help." },
|
|
22
22
|
"model-unavailable": { label: "Model unavailable", next: "This agent cannot use this model. Pick another from its model list." },
|
|
23
23
|
"setup-required": { label: "Needs setup", next: "Finish the agent's own setup, then check again." },
|
|
24
|
-
unreachable: { label: "
|
|
24
|
+
unreachable: { label: "Could not verify", next: "Model access is unknown. This does not confirm a sign-in or setup problem." },
|
|
25
25
|
updating: { label: "Updating", next: "An update is running. Check again when it finishes." },
|
|
26
|
-
unverified: { label: "Not
|
|
26
|
+
unverified: { label: "Not verified", next: "A task can run without a separate model check." },
|
|
27
27
|
ready: { label: "Ready", next: "" },
|
|
28
28
|
missing: { label: "Not available", next: "Not installed, or it cannot run without a terminal." },
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/** Agents that need the user come first, unavailable ones last. */
|
|
32
32
|
export function accessRank(key: AccessKey): number {
|
|
33
|
-
return key === "unverified" ? 1 : key === "ready" ? 2 : key === "missing" ? 3 : 0;
|
|
33
|
+
return key === "unverified" || key === "unreachable" ? 1 : key === "ready" ? 2 : key === "missing" ? 3 : 0;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export const HARNESS_GUIDE_URL = "https://ennodia.cherninlab.com/docs/reference/supported-harnesses/";
|
|
@@ -88,7 +88,7 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
88
88
|
function normalize(h) {
|
|
89
89
|
var r = h.readiness || {};
|
|
90
90
|
var key = h.runnable === false ? "missing" : (ACCESS[r.state] ? r.state : "unverified");
|
|
91
|
-
return { id: h.id, name: h.name || h.id, model: r.model || "", key: key,
|
|
91
|
+
return { id: h.id, name: h.name || h.id, model: r.model || "", message: r.message || "", key: key,
|
|
92
92
|
command: key === "login-required" && r.recovery && Array.isArray(r.recovery.command) ? r.recovery.command : [] };
|
|
93
93
|
}
|
|
94
94
|
function find(id) { for (var i = 0; i < state.agents.length; i++) if (state.agents[i].id === id) return state.agents[i]; return null; }
|
|
@@ -125,7 +125,7 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
125
125
|
if (data.installation) state.installation = data.installation;
|
|
126
126
|
merge(data.harnesses);
|
|
127
127
|
var now = find(agent.id);
|
|
128
|
-
state.notes[agent.id] = now && now.key === "ready" ? "Ready now." :
|
|
128
|
+
state.notes[agent.id] = now && now.key === "ready" ? "Ready now." : "";
|
|
129
129
|
if (now && now.key === "ready" && before !== "ready") state.newlyReady[agent.id] = true;
|
|
130
130
|
}, function (error) { state.notes[agent.id] = error.message || "The check failed."; })
|
|
131
131
|
.then(function () { delete state.busy[agent.id]; render(); });
|
|
@@ -178,12 +178,12 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
178
178
|
var head = el("div", "head");
|
|
179
179
|
head.appendChild(el("span", "dot"));
|
|
180
180
|
var name = el("span", "name", agent.name);
|
|
181
|
-
if (agent.model) name.appendChild(el("span", "model", agent.model));
|
|
181
|
+
if (agent.model) name.appendChild(el("span", "model", " · " + agent.model));
|
|
182
182
|
head.appendChild(name);
|
|
183
183
|
head.appendChild(el("span", "status", ACCESS[agent.key].label));
|
|
184
184
|
item.appendChild(head);
|
|
185
|
-
|
|
186
|
-
if (
|
|
185
|
+
var next = agent.key === "unverified" && agent.message ? agent.message : ACCESS[agent.key].next;
|
|
186
|
+
if (next) item.appendChild(el("p", "next", next));
|
|
187
187
|
if (agent.command.length) {
|
|
188
188
|
var row = el("div", "command");
|
|
189
189
|
var code = el("code", "", shellQuote(agent.command));
|
|
@@ -216,11 +216,11 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
216
216
|
install.textContent = "";
|
|
217
217
|
install.hidden = !report || report.status === "healthy";
|
|
218
218
|
if (!install.hidden) {
|
|
219
|
-
install.appendChild(el("p", "", "Ennodia installation: " + report.status + "."));
|
|
219
|
+
install.appendChild(el("p", "", report.status !== "broken" && report.restartRequired ? "Ennodia update ready." : "Ennodia installation: " + report.status + "."));
|
|
220
220
|
var notes = el("ul");
|
|
221
221
|
(report.limitations || []).forEach(function (text) { notes.appendChild(el("li", "", text)); });
|
|
222
222
|
install.appendChild(notes);
|
|
223
|
-
install.appendChild(button("Installation guide", "", function () { openLink(__INSTALL__); }));
|
|
223
|
+
if (!report.restartRequired || report.status === "broken") install.appendChild(button("Installation guide", "", function () { openLink(__INSTALL__); }));
|
|
224
224
|
}
|
|
225
225
|
document.getElementById("cost").hidden = !state.host.serverTools || !state.agents.some(function (a) { return a.key !== "ready" && a.key !== "missing"; });
|
|
226
226
|
resize();
|
package/src/tasks.ts
CHANGED
|
@@ -145,6 +145,8 @@ type InternalTask = Omit<
|
|
|
145
145
|
isolatedCwd?: string;
|
|
146
146
|
extractUsage?: HarnessAdapter["extractUsage"];
|
|
147
147
|
failureReason?: HarnessAdapter["failureReason"];
|
|
148
|
+
streamFailureReason?: HarnessAdapter["streamFailureReason"];
|
|
149
|
+
streamFailure?: string;
|
|
148
150
|
/** The exact spawn, kept so a start that met a startup conflict can run again. */
|
|
149
151
|
spawnInput: TaskSpawnInput;
|
|
150
152
|
stdinText?: string;
|
|
@@ -379,6 +381,7 @@ ${noticeLines.join("\n")}
|
|
|
379
381
|
isolatedCwd,
|
|
380
382
|
extractUsage: adapter.extractUsage,
|
|
381
383
|
failureReason: adapter.failureReason,
|
|
384
|
+
streamFailureReason: adapter.streamFailureReason,
|
|
382
385
|
spawnInput,
|
|
383
386
|
stdinText: commandSpec.stdin,
|
|
384
387
|
startupConflict: adapter.startupConflict,
|
|
@@ -407,7 +410,7 @@ ${noticeLines.join("\n")}
|
|
|
407
410
|
]).then(() => undefined);
|
|
408
411
|
|
|
409
412
|
task.timeout = setTimeout(() => {
|
|
410
|
-
if (task.cancelRequested) {
|
|
413
|
+
if (task.cancelRequested || task.streamFailure) {
|
|
411
414
|
return;
|
|
412
415
|
}
|
|
413
416
|
|
|
@@ -549,7 +552,7 @@ ${noticeLines.join("\n")}
|
|
|
549
552
|
terminalStatus = "cancelled";
|
|
550
553
|
} else {
|
|
551
554
|
terminalStatus =
|
|
552
|
-
exitCode === 0 && !task.timedOut && !task.drainTimedOut
|
|
555
|
+
exitCode === 0 && !task.timedOut && !task.drainTimedOut && !task.streamFailure
|
|
553
556
|
? "succeeded"
|
|
554
557
|
: "failed";
|
|
555
558
|
}
|
|
@@ -628,7 +631,7 @@ ${noticeLines.join("\n")}
|
|
|
628
631
|
private metStartupConflict(task: InternalTask): boolean {
|
|
629
632
|
return Boolean(task.startupConflict?.test(task.stderr)) &&
|
|
630
633
|
!task.stdout.trim() &&
|
|
631
|
-
!task.cancelRequested && !task.timedOut && !task.drainTimedOut && !this.shuttingDown &&
|
|
634
|
+
!task.cancelRequested && !task.timedOut && !task.drainTimedOut && !task.streamFailure && !this.shuttingDown &&
|
|
632
635
|
(task.startupRestarts ?? 0) < MAX_STARTUP_RESTARTS &&
|
|
633
636
|
Date.now() - task.attemptStartedAtMs < STARTUP_CONFLICT_WINDOW_MS;
|
|
634
637
|
}
|
|
@@ -903,6 +906,7 @@ ${noticeLines.join("\n")}
|
|
|
903
906
|
}
|
|
904
907
|
task.streamCaptureTruncated ||= task[streamName].length + chunk.length > MAX_CAPTURE_CHARS;
|
|
905
908
|
task[streamName] = appendLimited(task[streamName], chunk);
|
|
909
|
+
this.checkStreamFailure(task);
|
|
906
910
|
task.lastOutputAtMs = Date.now();
|
|
907
911
|
this.pushEvent(task, { type: streamName, message: chunk });
|
|
908
912
|
this.touch(task);
|
|
@@ -912,6 +916,7 @@ ${noticeLines.join("\n")}
|
|
|
912
916
|
if (finalChunk) {
|
|
913
917
|
task.streamCaptureTruncated ||= task[streamName].length + finalChunk.length > MAX_CAPTURE_CHARS;
|
|
914
918
|
task[streamName] = appendLimited(task[streamName], finalChunk);
|
|
919
|
+
this.checkStreamFailure(task);
|
|
915
920
|
task.lastOutputAtMs = Date.now();
|
|
916
921
|
this.pushEvent(task, { type: streamName, message: finalChunk });
|
|
917
922
|
this.touch(task);
|
|
@@ -922,6 +927,20 @@ ${noticeLines.join("\n")}
|
|
|
922
927
|
}
|
|
923
928
|
}
|
|
924
929
|
|
|
930
|
+
private checkStreamFailure(task: InternalTask): void {
|
|
931
|
+
if (!task.streamFailureReason || task.streamFailure || task.cancelRequested || task.timedOut) return;
|
|
932
|
+
try {
|
|
933
|
+
const reason = task.streamFailureReason(task.stdout, task.stderr);
|
|
934
|
+
if (!reason) return;
|
|
935
|
+
task.streamFailure = reason;
|
|
936
|
+
this.pushEvent(task, { type: "error", message: reason });
|
|
937
|
+
this.terminate(task);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
task.streamFailureReason = undefined;
|
|
940
|
+
this.pushEvent(task, { type: "error", message: `Adapter stream check failed: ${String(error)}` });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
925
944
|
private toView(task: InternalTask, options: TaskViewOptions = {}): TaskView {
|
|
926
945
|
const now = Date.now();
|
|
927
946
|
const end = task.endedAtMs ?? now;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ENNODIA_VERSION = "0.5.
|
|
1
|
+
export const ENNODIA_VERSION = "0.5.3";
|