@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.
Files changed (87) hide show
  1. package/PURPOSE.md +94 -0
  2. package/README.md +30 -0
  3. package/dist/adapters/index.d.ts +13 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +11 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/identity/auth-records.d.ts +147 -0
  8. package/dist/identity/auth-records.d.ts.map +1 -0
  9. package/dist/identity/auth-records.js +354 -0
  10. package/dist/identity/auth-records.js.map +1 -0
  11. package/dist/identity/ids.d.ts +57 -0
  12. package/dist/identity/ids.d.ts.map +1 -0
  13. package/dist/identity/ids.js +80 -0
  14. package/dist/identity/ids.js.map +1 -0
  15. package/dist/identity/index.d.ts +14 -0
  16. package/dist/identity/index.d.ts.map +1 -0
  17. package/dist/identity/index.js +14 -0
  18. package/dist/identity/index.js.map +1 -0
  19. package/dist/identity/labels.d.ts +115 -0
  20. package/dist/identity/labels.d.ts.map +1 -0
  21. package/dist/identity/labels.js +154 -0
  22. package/dist/identity/labels.js.map +1 -0
  23. package/dist/identity/layout.d.ts +188 -0
  24. package/dist/identity/layout.d.ts.map +1 -0
  25. package/dist/identity/layout.js +233 -0
  26. package/dist/identity/layout.js.map +1 -0
  27. package/dist/identity/ports.d.ts +31 -0
  28. package/dist/identity/ports.d.ts.map +1 -0
  29. package/dist/identity/ports.js +9 -0
  30. package/dist/identity/ports.js.map +1 -0
  31. package/dist/identity/product-state.d.ts +49 -0
  32. package/dist/identity/product-state.d.ts.map +1 -0
  33. package/dist/identity/product-state.js +55 -0
  34. package/dist/identity/product-state.js.map +1 -0
  35. package/dist/index.d.ts +19 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +19 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/supervision/app-health.d.ts +148 -0
  40. package/dist/supervision/app-health.d.ts.map +1 -0
  41. package/dist/supervision/app-health.js +165 -0
  42. package/dist/supervision/app-health.js.map +1 -0
  43. package/dist/supervision/app-restart.d.ts +105 -0
  44. package/dist/supervision/app-restart.d.ts.map +1 -0
  45. package/dist/supervision/app-restart.js +97 -0
  46. package/dist/supervision/app-restart.js.map +1 -0
  47. package/dist/supervision/health.d.ts +218 -0
  48. package/dist/supervision/health.d.ts.map +1 -0
  49. package/dist/supervision/health.js +227 -0
  50. package/dist/supervision/health.js.map +1 -0
  51. package/dist/supervision/index.d.ts +17 -0
  52. package/dist/supervision/index.d.ts.map +1 -0
  53. package/dist/supervision/index.js +17 -0
  54. package/dist/supervision/index.js.map +1 -0
  55. package/dist/supervision/service-registration.d.ts +343 -0
  56. package/dist/supervision/service-registration.d.ts.map +1 -0
  57. package/dist/supervision/service-registration.js +543 -0
  58. package/dist/supervision/service-registration.js.map +1 -0
  59. package/dist/supervision/update.d.ts +231 -0
  60. package/dist/supervision/update.d.ts.map +1 -0
  61. package/dist/supervision/update.js +336 -0
  62. package/dist/supervision/update.js.map +1 -0
  63. package/dist/transport/admission.d.ts +235 -0
  64. package/dist/transport/admission.d.ts.map +1 -0
  65. package/dist/transport/admission.js +313 -0
  66. package/dist/transport/admission.js.map +1 -0
  67. package/dist/transport/auth.d.ts +232 -0
  68. package/dist/transport/auth.d.ts.map +1 -0
  69. package/dist/transport/auth.js +266 -0
  70. package/dist/transport/auth.js.map +1 -0
  71. package/dist/transport/index.d.ts +21 -0
  72. package/dist/transport/index.d.ts.map +1 -0
  73. package/dist/transport/index.js +21 -0
  74. package/dist/transport/index.js.map +1 -0
  75. package/dist/transport/ingress.d.ts +94 -0
  76. package/dist/transport/ingress.d.ts.map +1 -0
  77. package/dist/transport/ingress.js +171 -0
  78. package/dist/transport/ingress.js.map +1 -0
  79. package/dist/transport/routing.d.ts +235 -0
  80. package/dist/transport/routing.d.ts.map +1 -0
  81. package/dist/transport/routing.js +288 -0
  82. package/dist/transport/routing.js.map +1 -0
  83. package/dist/transport/shipped-routes.d.ts +21 -0
  84. package/dist/transport/shipped-routes.d.ts.map +1 -0
  85. package/dist/transport/shipped-routes.js +53 -0
  86. package/dist/transport/shipped-routes.js.map +1 -0
  87. 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":"AAAA;;;;;;;;;;;;;GAaG;AAgBH,yCAAyC;AACzC,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,cAAc,EAAE,IAAI;IACpB,UAAU,EAAE,MAAM;IAClB,UAAU,EAAE,CAAC;IACb,QAAQ,EAAE,OAAO,EAAE,2BAA2B;CACtC,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,YAAqB,EAAE,GAAW;IAC9D,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAE,YAA0B,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/E,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,IAAI,EAAkB,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,GAAI,IAAe,IAAI,kBAAkB,CAAC,QAAQ,CACzG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,UAAkB;IAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;IAC7C,OAAO,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,cAAc,GAAG,CAAC,IAAI,QAAQ,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;AACpG,CAAC;AAuDD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAuB,EAAE,GAAW;IAClE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAEnD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS,KAAK,KAAK,EAAE,CAAC;QACtE,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClB,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/B,IAAI,OAAO,IAAI,kBAAkB,CAAC,UAAU,EAAE,CAAC;YAC7C,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,QAAQ,GAAG,MAAM,CAAC,CAAC;YACvE,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,GAAG,EAAE,QAAQ,KAAK,uBAAuB,OAAO,aAAa,aAAa,uCAAuC,OAAO,GAAG;gBAC3H,MAAM,EAAE;oBACN,MAAM,EAAE,UAAU;oBAClB,GAAG,EAAE,IAAI;oBACT,YAAY,EAAE,OAAO;oBACrB,KAAK,EAAE,WAAW,OAAO,aAAa,aAAa,gFAAgF,OAAO,EAAE;iBAC7I;aACF,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;QACxC,OAAO;YACL,IAAI,EAAE,SAAS;YACf,OAAO;YACP,GAAG,EAAE,QAAQ,KAAK,sBAAsB,IAAI,mBAAmB,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,YAAY,OAAO,IAAI,kBAAkB,CAAC,UAAU,GAAG;YAChJ,MAAM,EAAE;gBACN,MAAM,EAAE,YAAY;gBACpB,GAAG,EAAE,IAAI;gBACT,YAAY,EAAE,OAAO;gBACrB,KAAK,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,4BAA4B,IAAI,EAAE;aAC9D;SACF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,QAAQ,KAAK,sBAAsB,IAAI,EAAE;QAC9C,MAAM,EAAE;YACN,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO;YACxC,GAAG,EAAE,IAAI;YACT,KAAK,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,4BAA4B,IAAI,EAAE;SAC9D;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAiB;IAClD,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC"}