@amalgm/core 0.1.0
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/PURPOSE.md +94 -0
- package/README.md +30 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/identity/auth-records.d.ts +147 -0
- package/dist/identity/auth-records.d.ts.map +1 -0
- package/dist/identity/auth-records.js +354 -0
- package/dist/identity/auth-records.js.map +1 -0
- package/dist/identity/ids.d.ts +57 -0
- package/dist/identity/ids.d.ts.map +1 -0
- package/dist/identity/ids.js +80 -0
- package/dist/identity/ids.js.map +1 -0
- package/dist/identity/index.d.ts +14 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +14 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/labels.d.ts +115 -0
- package/dist/identity/labels.d.ts.map +1 -0
- package/dist/identity/labels.js +154 -0
- package/dist/identity/labels.js.map +1 -0
- package/dist/identity/layout.d.ts +188 -0
- package/dist/identity/layout.d.ts.map +1 -0
- package/dist/identity/layout.js +233 -0
- package/dist/identity/layout.js.map +1 -0
- package/dist/identity/ports.d.ts +31 -0
- package/dist/identity/ports.d.ts.map +1 -0
- package/dist/identity/ports.js +9 -0
- package/dist/identity/ports.js.map +1 -0
- package/dist/identity/product-state.d.ts +49 -0
- package/dist/identity/product-state.d.ts.map +1 -0
- package/dist/identity/product-state.js +55 -0
- package/dist/identity/product-state.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/supervision/app-health.d.ts +148 -0
- package/dist/supervision/app-health.d.ts.map +1 -0
- package/dist/supervision/app-health.js +165 -0
- package/dist/supervision/app-health.js.map +1 -0
- package/dist/supervision/app-restart.d.ts +105 -0
- package/dist/supervision/app-restart.d.ts.map +1 -0
- package/dist/supervision/app-restart.js +97 -0
- package/dist/supervision/app-restart.js.map +1 -0
- package/dist/supervision/health.d.ts +218 -0
- package/dist/supervision/health.d.ts.map +1 -0
- package/dist/supervision/health.js +227 -0
- package/dist/supervision/health.js.map +1 -0
- package/dist/supervision/index.d.ts +17 -0
- package/dist/supervision/index.d.ts.map +1 -0
- package/dist/supervision/index.js +17 -0
- package/dist/supervision/index.js.map +1 -0
- package/dist/supervision/service-registration.d.ts +343 -0
- package/dist/supervision/service-registration.d.ts.map +1 -0
- package/dist/supervision/service-registration.js +543 -0
- package/dist/supervision/service-registration.js.map +1 -0
- package/dist/supervision/update.d.ts +231 -0
- package/dist/supervision/update.d.ts.map +1 -0
- package/dist/supervision/update.js +336 -0
- package/dist/supervision/update.js.map +1 -0
- package/dist/transport/admission.d.ts +235 -0
- package/dist/transport/admission.d.ts.map +1 -0
- package/dist/transport/admission.js +313 -0
- package/dist/transport/admission.js.map +1 -0
- package/dist/transport/auth.d.ts +232 -0
- package/dist/transport/auth.d.ts.map +1 -0
- package/dist/transport/auth.js +266 -0
- package/dist/transport/auth.js.map +1 -0
- package/dist/transport/index.d.ts +21 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +21 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/ingress.d.ts +94 -0
- package/dist/transport/ingress.d.ts.map +1 -0
- package/dist/transport/ingress.js +171 -0
- package/dist/transport/ingress.js.map +1 -0
- package/dist/transport/routing.d.ts +235 -0
- package/dist/transport/routing.d.ts.map +1 -0
- package/dist/transport/routing.js +288 -0
- package/dist/transport/routing.js.map +1 -0
- package/dist/transport/shipped-routes.d.ts +21 -0
- package/dist/transport/shipped-routes.d.ts.map +1 -0
- package/dist/transport/shipped-routes.js +53 -0
- package/dist/transport/shipped-routes.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App health contract — the push side of supervision.
|
|
3
|
+
*
|
|
4
|
+
* THE INVERSION: apps announce their own liveness; the runtime never probes
|
|
5
|
+
* them. Two signals cover every failure mode — process exit events detect
|
|
6
|
+
* death (instant and free), and missed heartbeats detect zombies (alive but
|
|
7
|
+
* hung). An app that never opts in costs the runtime nothing beyond the
|
|
8
|
+
* exit watch. Liveness bookkeeping is memory-only and runtime-scoped: a
|
|
9
|
+
* restarted runtime starts blank and apps re-announce; only *transitions*
|
|
10
|
+
* (ready, running ↔ unresponsive) touch the persisted app record.
|
|
11
|
+
*
|
|
12
|
+
* Engine source (production until cutover, axiom 9):
|
|
13
|
+
* `runtime/scripts/amalgm-mcp/apps/health.js`, endpoint constants from
|
|
14
|
+
* `apps/rest.js` and `server/routes/apps.js`.
|
|
15
|
+
*/
|
|
16
|
+
/** The persisted app record fields this contract reads. */
|
|
17
|
+
export interface AppHealthRecord {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly status: AppHealthStatus;
|
|
20
|
+
readonly desiredState: 'running' | 'stopped';
|
|
21
|
+
readonly heartbeatIntervalMs?: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** App statuses as this module observes them (full vocabulary in app-restart.ts). */
|
|
24
|
+
export type AppHealthStatus = 'registered' | 'building' | 'running' | 'restarting' | 'degraded' | 'error' | 'stopped' | 'unresponsive';
|
|
25
|
+
/** Engine `apps/health.js:24-31`. */
|
|
26
|
+
export declare const APP_HEALTH_POLICY: {
|
|
27
|
+
readonly defaultIntervalMs: 30000;
|
|
28
|
+
readonly minIntervalMs: 5000;
|
|
29
|
+
readonly maxIntervalMs: 300000;
|
|
30
|
+
/** Beats an app may miss before it is considered unresponsive. */
|
|
31
|
+
readonly missedBeats: 3;
|
|
32
|
+
readonly sweepIntervalMs: 10000;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Engine `apps/health.js:37-41`. Note the exact numeric semantics:
|
|
36
|
+
* `Number(undefined)` is NaN → default, but `Number(null)` is 0 → clamped
|
|
37
|
+
* to `minIntervalMs`. Byte-exact behavior is the contract (axiom 8).
|
|
38
|
+
*/
|
|
39
|
+
export declare function clampIntervalMs(value: unknown): number;
|
|
40
|
+
/** Engine `apps/health.js:43-45`: silence beyond `intervalMs * missedBeats` means unresponsive. */
|
|
41
|
+
export declare function unresponsiveAfterMs(intervalMs: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Engine `apps/health.js:50-56`. A beat only counts for an app the
|
|
44
|
+
* supervisor wants running. Anything else (stopped, degraded, mid-restart)
|
|
45
|
+
* is a straggler process talking past its lifecycle; enrolling it would
|
|
46
|
+
* resurrect state the supervisor just settled.
|
|
47
|
+
*/
|
|
48
|
+
export declare function acceptsHealthSignals(app: AppHealthRecord | null | undefined): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Rejection vocabulary for ready/heartbeat signals (engine
|
|
51
|
+
* `apps/health.js:95-96,124-125`): `'not_found'` for a missing record,
|
|
52
|
+
* `` `status_${app.status}` `` for a record outside the accepting states.
|
|
53
|
+
* Returns null when the signal is accepted.
|
|
54
|
+
*/
|
|
55
|
+
export declare function healthSignalRejection(app: AppHealthRecord | null | undefined): string | null;
|
|
56
|
+
/** In-memory enrollment entry — epoch-ms, runtime-scoped, never persisted per-beat. */
|
|
57
|
+
export interface AppHealthEntry {
|
|
58
|
+
readonly readyAt: number | null;
|
|
59
|
+
readonly lastHeartbeatAt: number;
|
|
60
|
+
readonly intervalMs: number;
|
|
61
|
+
}
|
|
62
|
+
/** The live-view shape merged under `health` on list responses. */
|
|
63
|
+
export interface AppHealthSnapshot {
|
|
64
|
+
readonly appId: string;
|
|
65
|
+
readonly readyAt: string | null;
|
|
66
|
+
readonly lastHeartbeatAt: string | null;
|
|
67
|
+
readonly heartbeatIntervalMs: number;
|
|
68
|
+
readonly heartbeatAgeMs: number | null;
|
|
69
|
+
}
|
|
70
|
+
/** Engine `apps/health.js:58-66`: ISO timestamps, age clamped at 0. */
|
|
71
|
+
export declare function snapshotEntry(appId: string, entry: AppHealthEntry, now: number): AppHealthSnapshot;
|
|
72
|
+
export interface AppRecoveryDecision {
|
|
73
|
+
readonly log: string;
|
|
74
|
+
readonly update: {
|
|
75
|
+
readonly status: 'running';
|
|
76
|
+
readonly error: null;
|
|
77
|
+
readonly lastHeartbeatAt: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The recover rule (engine `apps/health.js:77-86`): a heartbeat from an
|
|
82
|
+
* `unresponsive` app flips it back to `running`. Null when no recovery
|
|
83
|
+
* applies. The log string is the engine's, byte for byte.
|
|
84
|
+
*/
|
|
85
|
+
export declare function heartbeatRecovery(app: AppHealthRecord, now: number): AppRecoveryDecision | null;
|
|
86
|
+
export type AppSweepTransition =
|
|
87
|
+
/**
|
|
88
|
+
* The app missed its beats while wanted and believed running: mark it
|
|
89
|
+
* `unresponsive`. Honest reporting, not a kill — the process is alive.
|
|
90
|
+
*/
|
|
91
|
+
{
|
|
92
|
+
readonly kind: 'unresponsive';
|
|
93
|
+
readonly appId: string;
|
|
94
|
+
readonly log: string;
|
|
95
|
+
readonly update: {
|
|
96
|
+
readonly status: 'unresponsive';
|
|
97
|
+
readonly error: string;
|
|
98
|
+
readonly lastHeartbeatAt: string;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Lifecycle moved on; the supervisor owns that state — drop the stale
|
|
103
|
+
* enrollment instead of fighting it. `engineParity` is true when the
|
|
104
|
+
* shipped engine also drops this enrollment (missing / stopped /
|
|
105
|
+
* degraded), false where this SDK closes the engine's leak (see below).
|
|
106
|
+
*/
|
|
107
|
+
| {
|
|
108
|
+
readonly kind: 'drop-enrollment';
|
|
109
|
+
readonly appId: string;
|
|
110
|
+
readonly engineParity: boolean;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* One monitor pass (engine `apps/health.js:164-188`) as a pure function
|
|
114
|
+
* over the enrollment map, an app lookup, and an injected clock. No process
|
|
115
|
+
* probing, no disk reads — the host applies the returned transitions
|
|
116
|
+
* (record update + route sync) and deletes dropped enrollments.
|
|
117
|
+
*
|
|
118
|
+
* The unresponsive error string is the engine's, byte for byte:
|
|
119
|
+
* `No heartbeat for ${silentSeconds}s (app declared one every
|
|
120
|
+
* ${Math.round(intervalMs / 1000)}s). Process is alive but not responding.`
|
|
121
|
+
*
|
|
122
|
+
* ENGINE LEAK, fixed here as the law (deviation from engine
|
|
123
|
+
* `apps/health.js:170-175`): the engine drops a silent app's enrollment
|
|
124
|
+
* only when the record is missing or its status is `stopped`/`degraded`.
|
|
125
|
+
* Statuses `restarting`/`error`/`building` skip the sweep but keep the
|
|
126
|
+
* enrollment forever — a zombie entry the sweep re-visits every pass. Those
|
|
127
|
+
* process generations are over or superseded, so this SDK drops them too
|
|
128
|
+
* (`engineParity: false` on those transitions). Enrollments for
|
|
129
|
+
* `unresponsive` apps are kept in both worlds — a resumed heartbeat must be
|
|
130
|
+
* able to recover them — as are `running` apps whose `desiredState` moved
|
|
131
|
+
* to `stopped` (the imminent exit clears them).
|
|
132
|
+
*/
|
|
133
|
+
export declare function sweepAppHealth(entries: Iterable<readonly [string, AppHealthEntry]>, appLookup: (appId: string) => AppHealthRecord | null | undefined, now: number): AppSweepTransition[];
|
|
134
|
+
/** Body key alias chain for the app id, in engine precedence order (`apps/rest.js:18-20`). */
|
|
135
|
+
export declare const APP_ID_BODY_KEYS: readonly ["app_id", "appId", "artifact_id", "artifactId"];
|
|
136
|
+
/** Engine `apps/rest.js:18-20`: `body?.app_id || body?.appId || body?.artifact_id || body?.artifactId`. */
|
|
137
|
+
export declare function appIdFromBody(body: Record<string, unknown> | null | undefined): unknown;
|
|
138
|
+
/** Body key alias chain for the heartbeat interval (`apps/rest.js:158`). */
|
|
139
|
+
export declare const HEARTBEAT_INTERVAL_BODY_KEYS: readonly ["heartbeat_interval_ms", "heartbeatIntervalMs"];
|
|
140
|
+
/** Engine `apps/rest.js:158`: `body?.heartbeat_interval_ms ?? body?.heartbeatIntervalMs`. */
|
|
141
|
+
export declare function heartbeatIntervalFromBody(body: Record<string, unknown> | null | undefined): unknown;
|
|
142
|
+
/** 400 body when no app id alias is present (`apps/rest.js:156,168`). */
|
|
143
|
+
export declare const APP_ID_REQUIRED_ERROR = "app_id is required";
|
|
144
|
+
export declare const APP_ID_REQUIRED_STATUS = 400;
|
|
145
|
+
/** 404 body when the signal names an unknown app (`apps/rest.js:160-162,170-172`). */
|
|
146
|
+
export declare const APP_NOT_FOUND_ERROR = "App not found";
|
|
147
|
+
export declare const APP_NOT_FOUND_STATUS = 404;
|
|
148
|
+
//# sourceMappingURL=app-health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-health.d.ts","sourceRoot":"","sources":["../../src/supervision/app-health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App health contract — the push side of supervision.
|
|
3
|
+
*
|
|
4
|
+
* THE INVERSION: apps announce their own liveness; the runtime never probes
|
|
5
|
+
* them. Two signals cover every failure mode — process exit events detect
|
|
6
|
+
* death (instant and free), and missed heartbeats detect zombies (alive but
|
|
7
|
+
* hung). An app that never opts in costs the runtime nothing beyond the
|
|
8
|
+
* exit watch. Liveness bookkeeping is memory-only and runtime-scoped: a
|
|
9
|
+
* restarted runtime starts blank and apps re-announce; only *transitions*
|
|
10
|
+
* (ready, running ↔ unresponsive) touch the persisted app record.
|
|
11
|
+
*
|
|
12
|
+
* Engine source (production until cutover, axiom 9):
|
|
13
|
+
* `runtime/scripts/amalgm-mcp/apps/health.js`, endpoint constants from
|
|
14
|
+
* `apps/rest.js` and `server/routes/apps.js`.
|
|
15
|
+
*/
|
|
16
|
+
/** Engine `apps/health.js:24-31`. */
|
|
17
|
+
export const APP_HEALTH_POLICY = {
|
|
18
|
+
defaultIntervalMs: 30_000,
|
|
19
|
+
minIntervalMs: 5_000,
|
|
20
|
+
maxIntervalMs: 300_000,
|
|
21
|
+
/** Beats an app may miss before it is considered unresponsive. */
|
|
22
|
+
missedBeats: 3,
|
|
23
|
+
sweepIntervalMs: 10_000,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Engine `apps/health.js:37-41`. Note the exact numeric semantics:
|
|
27
|
+
* `Number(undefined)` is NaN → default, but `Number(null)` is 0 → clamped
|
|
28
|
+
* to `minIntervalMs`. Byte-exact behavior is the contract (axiom 8).
|
|
29
|
+
*/
|
|
30
|
+
export function clampIntervalMs(value) {
|
|
31
|
+
const parsed = Number(value);
|
|
32
|
+
if (!Number.isFinite(parsed))
|
|
33
|
+
return APP_HEALTH_POLICY.defaultIntervalMs;
|
|
34
|
+
return Math.min(Math.max(parsed, APP_HEALTH_POLICY.minIntervalMs), APP_HEALTH_POLICY.maxIntervalMs);
|
|
35
|
+
}
|
|
36
|
+
/** Engine `apps/health.js:43-45`: silence beyond `intervalMs * missedBeats` means unresponsive. */
|
|
37
|
+
export function unresponsiveAfterMs(intervalMs) {
|
|
38
|
+
return intervalMs * APP_HEALTH_POLICY.missedBeats;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Engine `apps/health.js:50-56`. A beat only counts for an app the
|
|
42
|
+
* supervisor wants running. Anything else (stopped, degraded, mid-restart)
|
|
43
|
+
* is a straggler process talking past its lifecycle; enrolling it would
|
|
44
|
+
* resurrect state the supervisor just settled.
|
|
45
|
+
*/
|
|
46
|
+
export function acceptsHealthSignals(app) {
|
|
47
|
+
return Boolean(app
|
|
48
|
+
&& app.desiredState === 'running'
|
|
49
|
+
&& (app.status === 'running' || app.status === 'unresponsive'));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Rejection vocabulary for ready/heartbeat signals (engine
|
|
53
|
+
* `apps/health.js:95-96,124-125`): `'not_found'` for a missing record,
|
|
54
|
+
* `` `status_${app.status}` `` for a record outside the accepting states.
|
|
55
|
+
* Returns null when the signal is accepted.
|
|
56
|
+
*/
|
|
57
|
+
export function healthSignalRejection(app) {
|
|
58
|
+
if (!app)
|
|
59
|
+
return 'not_found';
|
|
60
|
+
if (!acceptsHealthSignals(app))
|
|
61
|
+
return `status_${app.status}`;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** Engine `apps/health.js:58-66`: ISO timestamps, age clamped at 0. */
|
|
65
|
+
export function snapshotEntry(appId, entry, now) {
|
|
66
|
+
return {
|
|
67
|
+
appId,
|
|
68
|
+
readyAt: entry.readyAt ? new Date(entry.readyAt).toISOString() : null,
|
|
69
|
+
lastHeartbeatAt: entry.lastHeartbeatAt ? new Date(entry.lastHeartbeatAt).toISOString() : null,
|
|
70
|
+
heartbeatIntervalMs: entry.intervalMs,
|
|
71
|
+
heartbeatAgeMs: entry.lastHeartbeatAt ? Math.max(0, now - entry.lastHeartbeatAt) : null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The recover rule (engine `apps/health.js:77-86`): a heartbeat from an
|
|
76
|
+
* `unresponsive` app flips it back to `running`. Null when no recovery
|
|
77
|
+
* applies. The log string is the engine's, byte for byte.
|
|
78
|
+
*/
|
|
79
|
+
export function heartbeatRecovery(app, now) {
|
|
80
|
+
if (app.status !== 'unresponsive')
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
log: `[App:${app.id}] heartbeat resumed; marking running again`,
|
|
84
|
+
update: {
|
|
85
|
+
status: 'running',
|
|
86
|
+
error: null,
|
|
87
|
+
lastHeartbeatAt: new Date(now).toISOString(),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One monitor pass (engine `apps/health.js:164-188`) as a pure function
|
|
93
|
+
* over the enrollment map, an app lookup, and an injected clock. No process
|
|
94
|
+
* probing, no disk reads — the host applies the returned transitions
|
|
95
|
+
* (record update + route sync) and deletes dropped enrollments.
|
|
96
|
+
*
|
|
97
|
+
* The unresponsive error string is the engine's, byte for byte:
|
|
98
|
+
* `No heartbeat for ${silentSeconds}s (app declared one every
|
|
99
|
+
* ${Math.round(intervalMs / 1000)}s). Process is alive but not responding.`
|
|
100
|
+
*
|
|
101
|
+
* ENGINE LEAK, fixed here as the law (deviation from engine
|
|
102
|
+
* `apps/health.js:170-175`): the engine drops a silent app's enrollment
|
|
103
|
+
* only when the record is missing or its status is `stopped`/`degraded`.
|
|
104
|
+
* Statuses `restarting`/`error`/`building` skip the sweep but keep the
|
|
105
|
+
* enrollment forever — a zombie entry the sweep re-visits every pass. Those
|
|
106
|
+
* process generations are over or superseded, so this SDK drops them too
|
|
107
|
+
* (`engineParity: false` on those transitions). Enrollments for
|
|
108
|
+
* `unresponsive` apps are kept in both worlds — a resumed heartbeat must be
|
|
109
|
+
* able to recover them — as are `running` apps whose `desiredState` moved
|
|
110
|
+
* to `stopped` (the imminent exit clears them).
|
|
111
|
+
*/
|
|
112
|
+
export function sweepAppHealth(entries, appLookup, now) {
|
|
113
|
+
const transitions = [];
|
|
114
|
+
for (const [appId, entry] of entries) {
|
|
115
|
+
if (now - entry.lastHeartbeatAt <= unresponsiveAfterMs(entry.intervalMs))
|
|
116
|
+
continue;
|
|
117
|
+
const app = appLookup(appId);
|
|
118
|
+
if (!app || app.desiredState !== 'running' || app.status !== 'running') {
|
|
119
|
+
if (!app || app.status === 'stopped' || app.status === 'degraded') {
|
|
120
|
+
transitions.push({ kind: 'drop-enrollment', appId, engineParity: true });
|
|
121
|
+
}
|
|
122
|
+
else if (app.status === 'restarting' || app.status === 'error' || app.status === 'building') {
|
|
123
|
+
transitions.push({ kind: 'drop-enrollment', appId, engineParity: false });
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const silentSeconds = Math.round((now - entry.lastHeartbeatAt) / 1000);
|
|
128
|
+
transitions.push({
|
|
129
|
+
kind: 'unresponsive',
|
|
130
|
+
appId,
|
|
131
|
+
log: `[App:${appId}] unresponsive: no heartbeat for ${silentSeconds}s (expected every ${Math.round(entry.intervalMs / 1000)}s)`,
|
|
132
|
+
update: {
|
|
133
|
+
status: 'unresponsive',
|
|
134
|
+
error: `No heartbeat for ${silentSeconds}s (app declared one every ${Math.round(entry.intervalMs / 1000)}s). Process is alive but not responding.`,
|
|
135
|
+
lastHeartbeatAt: new Date(entry.lastHeartbeatAt).toISOString(),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return transitions;
|
|
140
|
+
}
|
|
141
|
+
/* ------------------------------------------------------------------ *
|
|
142
|
+
* Endpoint contract (engine apps/rest.js:18-20,154-174 and
|
|
143
|
+
* server/routes/apps.js:5-13). Apps call POST /apps/ready and
|
|
144
|
+
* /apps/heartbeat with AMALGM_MCP_URL + AMALGM_RUNTIME_TOKEN already in
|
|
145
|
+
* their environment; /artifacts/* is the legacy alias of /apps/*.
|
|
146
|
+
* ------------------------------------------------------------------ */
|
|
147
|
+
/** Body key alias chain for the app id, in engine precedence order (`apps/rest.js:18-20`). */
|
|
148
|
+
export const APP_ID_BODY_KEYS = ['app_id', 'appId', 'artifact_id', 'artifactId'];
|
|
149
|
+
/** Engine `apps/rest.js:18-20`: `body?.app_id || body?.appId || body?.artifact_id || body?.artifactId`. */
|
|
150
|
+
export function appIdFromBody(body) {
|
|
151
|
+
return body?.['app_id'] || body?.['appId'] || body?.['artifact_id'] || body?.['artifactId'];
|
|
152
|
+
}
|
|
153
|
+
/** Body key alias chain for the heartbeat interval (`apps/rest.js:158`). */
|
|
154
|
+
export const HEARTBEAT_INTERVAL_BODY_KEYS = ['heartbeat_interval_ms', 'heartbeatIntervalMs'];
|
|
155
|
+
/** Engine `apps/rest.js:158`: `body?.heartbeat_interval_ms ?? body?.heartbeatIntervalMs`. */
|
|
156
|
+
export function heartbeatIntervalFromBody(body) {
|
|
157
|
+
return body?.['heartbeat_interval_ms'] ?? body?.['heartbeatIntervalMs'];
|
|
158
|
+
}
|
|
159
|
+
/** 400 body when no app id alias is present (`apps/rest.js:156,168`). */
|
|
160
|
+
export const APP_ID_REQUIRED_ERROR = 'app_id is required';
|
|
161
|
+
export const APP_ID_REQUIRED_STATUS = 400;
|
|
162
|
+
/** 404 body when the signal names an unknown app (`apps/rest.js:160-162,170-172`). */
|
|
163
|
+
export const APP_NOT_FOUND_ERROR = 'App not found';
|
|
164
|
+
export const APP_NOT_FOUND_STATUS = 404;
|
|
165
|
+
//# sourceMappingURL=app-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-health.js","sourceRoot":"","sources":["../../src/supervision/app-health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App crash breaker — restart policy for supervised apps.
|
|
3
|
+
*
|
|
4
|
+
* Crash containment: an app that keeps dying must not keep the supervisor
|
|
5
|
+
* busy forever. Restarts back off exponentially, and after `maxCrashes`
|
|
6
|
+
* unexpected exits inside `windowMs` the app is parked as `degraded` until
|
|
7
|
+
* someone starts it explicitly. THE CRASH HISTORY LIVES ON THE APP RECORD
|
|
8
|
+
* (apps.json), not in the supervisor process: a degraded app stays parked
|
|
9
|
+
* across runtime restarts — otherwise a runtime-level restart loop hands
|
|
10
|
+
* every broken app a fresh crash budget.
|
|
11
|
+
*
|
|
12
|
+
* Engine source (production until cutover, axiom 9):
|
|
13
|
+
* `runtime/scripts/amalgm-mcp/apps/supervisor.js`.
|
|
14
|
+
*/
|
|
15
|
+
/** The full app status vocabulary (axiom 8 — exactly these eight). */
|
|
16
|
+
export type AppStatus = 'registered' | 'building' | 'running' | 'restarting' | 'degraded' | 'error' | 'stopped' | 'unresponsive';
|
|
17
|
+
/** What the supervisor wants for the app, independent of what is true. */
|
|
18
|
+
export type AppDesiredState = 'running' | 'stopped';
|
|
19
|
+
/** Engine `apps/supervisor.js:37-42`. */
|
|
20
|
+
export declare const APP_RESTART_POLICY: {
|
|
21
|
+
readonly initialDelayMs: 2000;
|
|
22
|
+
readonly maxDelayMs: 60000;
|
|
23
|
+
readonly maxCrashes: 5;
|
|
24
|
+
readonly windowMs: 300000;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Engine `apps/supervisor.js:44-47`: only finite timestamps inside the
|
|
28
|
+
* window count. The stored history may carry junk from older versions —
|
|
29
|
+
* filtering is the read, not a repair pass.
|
|
30
|
+
*/
|
|
31
|
+
export declare function recentCrashes(crashHistory: unknown, now: number): number[];
|
|
32
|
+
/**
|
|
33
|
+
* Engine `apps/supervisor.js:49-52`:
|
|
34
|
+
* `min(initialDelayMs * 2 ** (crashCount - 1), maxDelayMs)` — the first
|
|
35
|
+
* crash restarts after 2s, then 4s, 8s, ... capped at 60s.
|
|
36
|
+
*/
|
|
37
|
+
export declare function restartDelayMs(crashCount: number): number;
|
|
38
|
+
/** The app record fields the exit decision reads. */
|
|
39
|
+
export interface AppExitContext {
|
|
40
|
+
readonly appId: string;
|
|
41
|
+
readonly desiredState: AppDesiredState;
|
|
42
|
+
/** Engine treats anything but the literal `false` as keep-alive (`apps/supervisor.js:361`). */
|
|
43
|
+
readonly keepAlive?: boolean | undefined;
|
|
44
|
+
readonly crashHistory: unknown;
|
|
45
|
+
/** Exit code of the dead process; null when killed by signal. */
|
|
46
|
+
readonly exitCode: number | null;
|
|
47
|
+
/** The app's log file path — it is quoted verbatim in the degraded error. */
|
|
48
|
+
readonly logPath: string;
|
|
49
|
+
}
|
|
50
|
+
export type AppExitDecision =
|
|
51
|
+
/**
|
|
52
|
+
* The crash budget is spent (engine `apps/supervisor.js:365-376`): park
|
|
53
|
+
* the app `degraded`, persist the crash history, pause automatic
|
|
54
|
+
* restarts. Only an explicit start clears it. Error and log strings are
|
|
55
|
+
* the engine's, byte for byte.
|
|
56
|
+
*/
|
|
57
|
+
{
|
|
58
|
+
readonly kind: 'degrade';
|
|
59
|
+
readonly log: string;
|
|
60
|
+
readonly update: {
|
|
61
|
+
readonly status: 'degraded';
|
|
62
|
+
readonly pid: null;
|
|
63
|
+
readonly crashHistory: readonly number[];
|
|
64
|
+
readonly error: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Restart after `delayMs` with the crash recorded (engine `apps/supervisor.js:377-393`). */
|
|
68
|
+
| {
|
|
69
|
+
readonly kind: 'restart';
|
|
70
|
+
readonly delayMs: number;
|
|
71
|
+
readonly log: string;
|
|
72
|
+
readonly update: {
|
|
73
|
+
readonly status: 'restarting';
|
|
74
|
+
readonly pid: null;
|
|
75
|
+
readonly crashHistory: readonly number[];
|
|
76
|
+
readonly error: string | null;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The exit was wanted (or keep-alive is off): settle as stopped/error (engine `apps/supervisor.js:396-401`). */
|
|
80
|
+
| {
|
|
81
|
+
readonly kind: 'settle';
|
|
82
|
+
readonly log: string;
|
|
83
|
+
readonly update: {
|
|
84
|
+
readonly status: 'stopped' | 'error';
|
|
85
|
+
readonly pid: null;
|
|
86
|
+
readonly error: string | null;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The decision half of the app exit handler (engine
|
|
91
|
+
* `apps/supervisor.js:356-402`). The host has already handled the
|
|
92
|
+
* was-stopping case and cleared the health enrollment (the process
|
|
93
|
+
* generation is over; the next one must re-announce).
|
|
94
|
+
*/
|
|
95
|
+
export declare function appExitDecision(context: AppExitContext, now: number): AppExitDecision;
|
|
96
|
+
/**
|
|
97
|
+
* Explicit-start reset (engine `apps/supervisor.js:415-417`). A degraded
|
|
98
|
+
* app has no pending restart timer, so an explicit start (user/API) is the
|
|
99
|
+
* only way it runs again — and it arrives with a fresh crash budget.
|
|
100
|
+
* Returns the record update to apply, or null when no reset is due.
|
|
101
|
+
*/
|
|
102
|
+
export declare function explicitStartReset(status: AppStatus): {
|
|
103
|
+
readonly crashHistory: [];
|
|
104
|
+
} | null;
|
|
105
|
+
//# sourceMappingURL=app-restart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-restart.d.ts","sourceRoot":"","sources":["../../src/supervision/app-restart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sEAAsE;AACtE,MAAM,MAAM,SAAS,GACjB,YAAY,GACZ,UAAU,GACV,SAAS,GACT,YAAY,GACZ,UAAU,GACV,OAAO,GACP,SAAS,GACT,cAAc,CAAC;AAEnB,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;AAEpD,yCAAyC;AACzC,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AAEX;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAK1E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,eAAe;AACzB;;;;;GAKG;AACD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAC5B,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;QACnB,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;QACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AACH,6FAA6F;GAC3F;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;QAC9B,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;QACnB,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;QACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,CAAC;CACH;AACH,iHAAiH;GAC/G;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;QACrC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;QACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,CAAC;CACH,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,CA2CrF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG;IAAE,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAA;CAAE,GAAG,IAAI,CAE1F"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App crash breaker — restart policy for supervised apps.
|
|
3
|
+
*
|
|
4
|
+
* Crash containment: an app that keeps dying must not keep the supervisor
|
|
5
|
+
* busy forever. Restarts back off exponentially, and after `maxCrashes`
|
|
6
|
+
* unexpected exits inside `windowMs` the app is parked as `degraded` until
|
|
7
|
+
* someone starts it explicitly. THE CRASH HISTORY LIVES ON THE APP RECORD
|
|
8
|
+
* (apps.json), not in the supervisor process: a degraded app stays parked
|
|
9
|
+
* across runtime restarts — otherwise a runtime-level restart loop hands
|
|
10
|
+
* every broken app a fresh crash budget.
|
|
11
|
+
*
|
|
12
|
+
* Engine source (production until cutover, axiom 9):
|
|
13
|
+
* `runtime/scripts/amalgm-mcp/apps/supervisor.js`.
|
|
14
|
+
*/
|
|
15
|
+
/** Engine `apps/supervisor.js:37-42`. */
|
|
16
|
+
export const APP_RESTART_POLICY = {
|
|
17
|
+
initialDelayMs: 2000,
|
|
18
|
+
maxDelayMs: 60_000,
|
|
19
|
+
maxCrashes: 5,
|
|
20
|
+
windowMs: 300_000, // 5 * 60_000 in the engine
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Engine `apps/supervisor.js:44-47`: only finite timestamps inside the
|
|
24
|
+
* window count. The stored history may carry junk from older versions —
|
|
25
|
+
* filtering is the read, not a repair pass.
|
|
26
|
+
*/
|
|
27
|
+
export function recentCrashes(crashHistory, now) {
|
|
28
|
+
const history = Array.isArray(crashHistory) ? crashHistory : [];
|
|
29
|
+
return history.filter((time) => Number.isFinite(time) && now - time <= APP_RESTART_POLICY.windowMs);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Engine `apps/supervisor.js:49-52`:
|
|
33
|
+
* `min(initialDelayMs * 2 ** (crashCount - 1), maxDelayMs)` — the first
|
|
34
|
+
* crash restarts after 2s, then 4s, 8s, ... capped at 60s.
|
|
35
|
+
*/
|
|
36
|
+
export function restartDelayMs(crashCount) {
|
|
37
|
+
const exponent = Math.max(0, crashCount - 1);
|
|
38
|
+
return Math.min(APP_RESTART_POLICY.initialDelayMs * 2 ** exponent, APP_RESTART_POLICY.maxDelayMs);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The decision half of the app exit handler (engine
|
|
42
|
+
* `apps/supervisor.js:356-402`). The host has already handled the
|
|
43
|
+
* was-stopping case and cleared the health enrollment (the process
|
|
44
|
+
* generation is over; the next one must re-announce).
|
|
45
|
+
*/
|
|
46
|
+
export function appExitDecision(context, now) {
|
|
47
|
+
const { appId, exitCode: code, logPath } = context;
|
|
48
|
+
if (context.desiredState === 'running' && context.keepAlive !== false) {
|
|
49
|
+
const history = recentCrashes(context.crashHistory, now);
|
|
50
|
+
history.push(now);
|
|
51
|
+
const crashes = history.length;
|
|
52
|
+
if (crashes >= APP_RESTART_POLICY.maxCrashes) {
|
|
53
|
+
const windowMinutes = Math.round(APP_RESTART_POLICY.windowMs / 60_000);
|
|
54
|
+
return {
|
|
55
|
+
kind: 'degrade',
|
|
56
|
+
log: `[App:${appId}] degraded: crashed ${crashes} times in ${windowMinutes}m; automatic restarts paused (logs: ${logPath})`,
|
|
57
|
+
update: {
|
|
58
|
+
status: 'degraded',
|
|
59
|
+
pid: null,
|
|
60
|
+
crashHistory: history,
|
|
61
|
+
error: `Crashed ${crashes} times in ${windowMinutes} minutes; automatic restarts paused. Fix the app, then start it again. Logs: ${logPath}`,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const delayMs = restartDelayMs(crashes);
|
|
66
|
+
return {
|
|
67
|
+
kind: 'restart',
|
|
68
|
+
delayMs,
|
|
69
|
+
log: `[App:${appId}] exited with code ${code}; restarting in ${Math.round(delayMs / 1000)}s (crash ${crashes}/${APP_RESTART_POLICY.maxCrashes})`,
|
|
70
|
+
update: {
|
|
71
|
+
status: 'restarting',
|
|
72
|
+
pid: null,
|
|
73
|
+
crashHistory: history,
|
|
74
|
+
error: code === 0 ? null : `Process exited with code ${code}`,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
kind: 'settle',
|
|
80
|
+
log: `[App:${appId}] exited with code ${code}`,
|
|
81
|
+
update: {
|
|
82
|
+
status: code === 0 ? 'stopped' : 'error',
|
|
83
|
+
pid: null,
|
|
84
|
+
error: code === 0 ? null : `Process exited with code ${code}`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Explicit-start reset (engine `apps/supervisor.js:415-417`). A degraded
|
|
90
|
+
* app has no pending restart timer, so an explicit start (user/API) is the
|
|
91
|
+
* only way it runs again — and it arrives with a fresh crash budget.
|
|
92
|
+
* Returns the record update to apply, or null when no reset is due.
|
|
93
|
+
*/
|
|
94
|
+
export function explicitStartReset(status) {
|
|
95
|
+
return status === 'degraded' ? { crashHistory: [] } : null;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=app-restart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-restart.js","sourceRoot":"","sources":["../../src/supervision/app-restart.ts"],"names":[],"mappings":"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"}
|