@hotmeshio/long-tail 0.25.3 → 0.26.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 (145) hide show
  1. package/README.md +1 -0
  2. package/build/index.d.ts +1 -0
  3. package/build/index.js +8 -1
  4. package/build/lib/machines/index.d.ts +5 -0
  5. package/build/lib/machines/index.js +21 -0
  6. package/build/lib/machines/line.d.ts +44 -0
  7. package/build/lib/machines/line.js +114 -0
  8. package/build/lib/machines/protocol/codec.d.ts +92 -0
  9. package/build/lib/machines/protocol/codec.js +79 -0
  10. package/build/lib/machines/protocol/constants.d.ts +120 -0
  11. package/build/lib/machines/protocol/constants.js +97 -0
  12. package/build/lib/machines/protocol/contract.d.ts +19 -0
  13. package/build/lib/machines/protocol/contract.js +132 -0
  14. package/build/lib/machines/protocol/envelope.d.ts +14 -0
  15. package/build/lib/machines/protocol/envelope.js +43 -0
  16. package/build/lib/machines/protocol/index.d.ts +5 -0
  17. package/build/lib/machines/protocol/index.js +21 -0
  18. package/build/lib/machines/protocol/topics.d.ts +33 -0
  19. package/build/lib/machines/protocol/topics.js +79 -0
  20. package/build/lib/machines/site/mosquitto-conf.d.ts +27 -0
  21. package/build/lib/machines/site/mosquitto-conf.js +81 -0
  22. package/build/lib/machines/site/site-conf.d.ts +20 -0
  23. package/build/lib/machines/site/site-conf.js +55 -0
  24. package/build/lib/machines/transport/callback-feed.d.ts +18 -0
  25. package/build/lib/machines/transport/callback-feed.js +31 -0
  26. package/build/lib/machines/transport/delivery.d.ts +21 -0
  27. package/build/lib/machines/transport/delivery.js +42 -0
  28. package/build/lib/machines/transport/index.d.ts +11 -0
  29. package/build/lib/machines/transport/index.js +27 -0
  30. package/build/lib/machines/transport/memory-broker.d.ts +35 -0
  31. package/build/lib/machines/transport/memory-broker.js +145 -0
  32. package/build/lib/machines/transport/memory.d.ts +40 -0
  33. package/build/lib/machines/transport/memory.js +79 -0
  34. package/build/lib/machines/transport/mqtt-bus.d.ts +24 -0
  35. package/build/lib/machines/transport/mqtt-bus.js +43 -0
  36. package/build/lib/machines/transport/mqtt-link.d.ts +16 -0
  37. package/build/lib/machines/transport/mqtt-link.js +37 -0
  38. package/build/lib/machines/transport/mqtt-session.d.ts +34 -0
  39. package/build/lib/machines/transport/mqtt-session.js +127 -0
  40. package/build/lib/machines/transport/nats-bus.d.ts +42 -0
  41. package/build/lib/machines/transport/nats-bus.js +67 -0
  42. package/build/lib/machines/transport/nats-durable.d.ts +11 -0
  43. package/build/lib/machines/transport/nats-durable.js +28 -0
  44. package/build/lib/machines/transport/nats-feed.d.ts +26 -0
  45. package/build/lib/machines/transport/nats-feed.js +49 -0
  46. package/build/lib/machines/transport/serial.d.ts +21 -0
  47. package/build/lib/machines/transport/serial.js +54 -0
  48. package/build/lib/machines/transport/streams.d.ts +28 -0
  49. package/build/lib/machines/transport/streams.js +92 -0
  50. package/build/routes/oauth-server/authorize.js +4 -4
  51. package/build/scripts/machine-line.d.ts +33 -0
  52. package/build/scripts/machine-line.js +301 -0
  53. package/build/services/auth/oauth-server/authorization.d.ts +5 -0
  54. package/build/services/auth/oauth-server/authorization.js +8 -0
  55. package/build/services/auth/oauth-server/metadata.d.ts +1 -1
  56. package/build/services/auth/oauth-server/metadata.js +2 -1
  57. package/build/services/machines/commands.d.ts +29 -0
  58. package/build/services/machines/commands.js +37 -0
  59. package/build/services/machines/config.d.ts +43 -0
  60. package/build/services/machines/config.js +84 -0
  61. package/build/services/machines/contracts.d.ts +193 -0
  62. package/build/services/machines/contracts.js +115 -0
  63. package/build/services/machines/forms.d.ts +55 -0
  64. package/build/services/machines/forms.js +37 -0
  65. package/build/services/machines/gateway/actions.d.ts +111 -0
  66. package/build/services/machines/gateway/actions.js +8 -0
  67. package/build/services/machines/gateway/address-book.d.ts +16 -0
  68. package/build/services/machines/gateway/address-book.js +24 -0
  69. package/build/services/machines/gateway/admission.d.ts +35 -0
  70. package/build/services/machines/gateway/admission.js +66 -0
  71. package/build/services/machines/gateway/api-escalations.d.ts +23 -0
  72. package/build/services/machines/gateway/api-escalations.js +101 -0
  73. package/build/services/machines/gateway/dispatcher.d.ts +63 -0
  74. package/build/services/machines/gateway/dispatcher.js +157 -0
  75. package/build/services/machines/gateway/escalations.d.ts +51 -0
  76. package/build/services/machines/gateway/escalations.js +40 -0
  77. package/build/services/machines/gateway/executor.d.ts +55 -0
  78. package/build/services/machines/gateway/executor.js +195 -0
  79. package/build/services/machines/gateway/gateway.d.ts +82 -0
  80. package/build/services/machines/gateway/gateway.js +247 -0
  81. package/build/services/machines/gateway/index.d.ts +10 -0
  82. package/build/services/machines/gateway/index.js +26 -0
  83. package/build/services/machines/gateway/jobs.d.ts +16 -0
  84. package/build/services/machines/gateway/jobs.js +24 -0
  85. package/build/services/machines/gateway/log.d.ts +41 -0
  86. package/build/services/machines/gateway/log.js +42 -0
  87. package/build/services/machines/gateway/lt-http.d.ts +51 -0
  88. package/build/services/machines/gateway/lt-http.js +120 -0
  89. package/build/services/machines/gateway/pacing.d.ts +29 -0
  90. package/build/services/machines/gateway/pacing.js +134 -0
  91. package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
  92. package/build/services/machines/gateway/plan-escalation.js +201 -0
  93. package/build/services/machines/gateway/plan-machine.d.ts +16 -0
  94. package/build/services/machines/gateway/plan-machine.js +242 -0
  95. package/build/services/machines/gateway/plan-shared.d.ts +18 -0
  96. package/build/services/machines/gateway/plan-shared.js +36 -0
  97. package/build/services/machines/gateway/provision.d.ts +22 -0
  98. package/build/services/machines/gateway/provision.js +79 -0
  99. package/build/services/machines/gateway/receipts.d.ts +33 -0
  100. package/build/services/machines/gateway/receipts.js +65 -0
  101. package/build/services/machines/gateway/reconcile.d.ts +33 -0
  102. package/build/services/machines/gateway/reconcile.js +86 -0
  103. package/build/services/machines/gateway/registration.d.ts +38 -0
  104. package/build/services/machines/gateway/registration.js +121 -0
  105. package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
  106. package/build/services/machines/gateway/reply-waiter.js +65 -0
  107. package/build/services/machines/gateway/state.d.ts +125 -0
  108. package/build/services/machines/gateway/state.js +228 -0
  109. package/build/services/machines/gateway/trace.d.ts +16 -0
  110. package/build/services/machines/gateway/trace.js +43 -0
  111. package/build/services/machines/identities.d.ts +25 -0
  112. package/build/services/machines/identities.js +77 -0
  113. package/build/services/machines/index.d.ts +11 -0
  114. package/build/services/machines/index.js +27 -0
  115. package/build/services/machines/leadership.d.ts +22 -0
  116. package/build/services/machines/leadership.js +75 -0
  117. package/build/services/machines/roles.d.ts +13 -0
  118. package/build/services/machines/roles.js +62 -0
  119. package/build/services/machines/rows.d.ts +30 -0
  120. package/build/services/machines/rows.js +56 -0
  121. package/build/services/machines/select.d.ts +18 -0
  122. package/build/services/machines/select.js +63 -0
  123. package/build/services/machines/surface.d.ts +33 -0
  124. package/build/services/machines/surface.js +98 -0
  125. package/build/services/machines/testing/fake-long-tail.d.ts +78 -0
  126. package/build/services/machines/testing/fake-long-tail.js +155 -0
  127. package/build/services/machines/testing/index.d.ts +3 -0
  128. package/build/services/machines/testing/index.js +19 -0
  129. package/build/services/machines/testing/recording-log.d.ts +14 -0
  130. package/build/services/machines/testing/recording-log.js +22 -0
  131. package/build/services/machines/testing/wait.d.ts +3 -0
  132. package/build/services/machines/testing/wait.js +17 -0
  133. package/build/services/mcp/db-server/schemas.d.ts +2 -2
  134. package/build/start/index.js +13 -0
  135. package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
  136. package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
  137. package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
  138. package/build/tsconfig.tsbuildinfo +1 -1
  139. package/build/types/index.d.ts +2 -1
  140. package/build/types/machines.d.ts +166 -0
  141. package/build/types/machines.js +7 -0
  142. package/build/types/startup.d.ts +23 -0
  143. package/docs/machines.md +293 -0
  144. package/docs/mcp.md +6 -1
  145. package/package.json +11 -1
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.silentLog = exports.consoleLog = exports.GATEWAY_LOG = void 0;
4
+ exports.GATEWAY_LOG = {
5
+ STARTED: 'machine_gateway.started',
6
+ RECONCILED: 'machine_gateway.reconciled',
7
+ DISPATCHED: 'machine_gateway.dispatch.accepted',
8
+ DISPATCH_REFUSED: 'machine_gateway.dispatch.refused',
9
+ DISPATCH_READY: 'machine_gateway.dispatch.ready',
10
+ DISPATCH_FAILED: 'machine_gateway.dispatch.failed',
11
+ DISPATCH_RECONCILED: 'machine_gateway.dispatch.reconciled',
12
+ VERB_INVALID: 'machine_gateway.verb.invalid',
13
+ VERB_FAILED: 'machine_gateway.verb.failed',
14
+ ROW_NOT_VISIBLE: 'machine_gateway.verb.row_not_visible',
15
+ ROW_CLAIMED_ELSEWHERE: 'machine_gateway.verb.row_claimed_elsewhere',
16
+ UNMATCHED: 'machine_gateway.message.unmatched',
17
+ UNREADABLE: 'machine_gateway.message.unreadable',
18
+ CONTRACT_VIOLATION: 'machine_gateway.contract.violation',
19
+ VIOLATION_ROW: 'machine_gateway.contract.row_created',
20
+ ADDRESS_BOUND: 'machine_gateway.address.bound',
21
+ ADDRESS_CONTESTED: 'machine_gateway.address.contested',
22
+ ADDRESS_UNKNOWN: 'machine_gateway.address.unknown',
23
+ ADDRESS_REFUSED: 'machine_gateway.address.refused',
24
+ RECEIPT_FAILED: 'machine_gateway.receipt.failed',
25
+ ALARM_ROW: 'machine_gateway.alarm.row_created',
26
+ OFFLINE_ROW: 'machine_gateway.offline.row_created',
27
+ QUIET_ROW: 'machine_gateway.quiet.row_created',
28
+ COMMANDED: 'machine_gateway.command.accepted',
29
+ COMMAND_REFUSED: 'machine_gateway.command.refused',
30
+ COMMAND_UNANSWERED: 'machine_gateway.command.unanswered',
31
+ COMMAND_UNSUPPORTED: 'machine_gateway.command.unsupported',
32
+ LEADING: 'machine_gateway.line.leading',
33
+ STANDBY: 'machine_gateway.line.standby',
34
+ LEADERSHIP_LOST: 'machine_gateway.line.leadership_lost',
35
+ START_FAILED: 'machine_gateway.line.start_failed',
36
+ };
37
+ exports.consoleLog = {
38
+ info: (event, context) => console.log(JSON.stringify({ level: 'info', event, ...context })),
39
+ warn: (event, context) => console.warn(JSON.stringify({ level: 'warn', event, ...context })),
40
+ error: (event, context) => console.error(JSON.stringify({ level: 'error', event, ...context })),
41
+ };
42
+ exports.silentLog = { info: () => undefined, warn: () => undefined, error: () => undefined };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The gateway's HTTP client to Long Tail. A non-2xx response throws
3
+ * LtHttpError with the status, body and request; the escalation verbs turn
4
+ * the outcomes that mean "already done" (404, 409) and "invalid" (422) into
5
+ * VerbResults and let every other failure throw, so the message that caused
6
+ * it is delivered again.
7
+ */
8
+ import type { OpenRowQuery } from '../rows';
9
+ import { type Actor, type CreateRowInput } from './actions';
10
+ import { type LineEscalations, type RowItem, type VerbResult } from './escalations';
11
+ export declare class LtHttpError extends Error {
12
+ readonly status: number | undefined;
13
+ readonly body: unknown;
14
+ readonly context: {
15
+ method: string;
16
+ path: string;
17
+ elapsedMs: number;
18
+ };
19
+ readonly cause?: unknown;
20
+ constructor(message: string, status: number | undefined, body: unknown, context: {
21
+ method: string;
22
+ path: string;
23
+ elapsedMs: number;
24
+ }, options?: {
25
+ cause?: unknown;
26
+ });
27
+ }
28
+ export declare class LtHttp {
29
+ private readonly baseUrl;
30
+ private readonly timeoutMs;
31
+ constructor(baseUrl: string, timeoutMs?: number);
32
+ request<T = any>(method: string, path: string, token: string | null, body?: unknown): Promise<T>;
33
+ }
34
+ /** Escalation verbs over HTTP, each run with the bearer token of the actor it runs as. */
35
+ export declare class HttpLineEscalations implements LineEscalations {
36
+ private readonly http;
37
+ private readonly tokenFor;
38
+ private readonly claimMinutes;
39
+ constructor(http: LtHttp, tokenFor: (as: Actor) => string, claimMinutes?: number);
40
+ claim(as: Actor, id: string): Promise<VerbResult>;
41
+ accumulate(as: Actor, id: string, itemKey: string, payload: Record<string, any>, metadata?: Record<string, any>): Promise<VerbResult>;
42
+ resolve(as: Actor, id: string, payload: Record<string, any>): Promise<VerbResult>;
43
+ create(as: Actor, input: CreateRowInput): Promise<{
44
+ id: string;
45
+ }>;
46
+ itemKeys(id: string): Promise<string[]>;
47
+ items(id: string): Promise<RowItem[]>;
48
+ get(id: string): Promise<Record<string, any> | null>;
49
+ listOpen(filter: OpenRowQuery): Promise<Record<string, any>[]>;
50
+ private verb;
51
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * The gateway's HTTP client to Long Tail. A non-2xx response throws
4
+ * LtHttpError with the status, body and request; the escalation verbs turn
5
+ * the outcomes that mean "already done" (404, 409) and "invalid" (422) into
6
+ * VerbResults and let every other failure throw, so the message that caused
7
+ * it is delivered again.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.HttpLineEscalations = exports.LtHttp = exports.LtHttpError = void 0;
11
+ const uuid_1 = require("../../../lib/uuid");
12
+ const actions_1 = require("./actions");
13
+ const escalations_1 = require("./escalations");
14
+ class LtHttpError extends Error {
15
+ constructor(message, status, body, context, options) {
16
+ super(message);
17
+ this.status = status;
18
+ this.body = body;
19
+ this.context = context;
20
+ this.name = 'LtHttpError';
21
+ this.cause = options?.cause;
22
+ }
23
+ }
24
+ exports.LtHttpError = LtHttpError;
25
+ class LtHttp {
26
+ constructor(baseUrl, timeoutMs = 10_000) {
27
+ this.baseUrl = baseUrl;
28
+ this.timeoutMs = timeoutMs;
29
+ }
30
+ async request(method, path, token, body) {
31
+ const started = performance.now();
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
34
+ const context = () => ({ method, path, elapsedMs: Math.round(performance.now() - started) });
35
+ let res;
36
+ try {
37
+ res = await fetch(`${this.baseUrl}/api${path}`, {
38
+ method,
39
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
40
+ body: body === undefined ? undefined : JSON.stringify(body),
41
+ signal: controller.signal,
42
+ });
43
+ }
44
+ catch (err) {
45
+ const reason = controller.signal.aborted ? `timed out after ${this.timeoutMs} ms` : err.message;
46
+ throw new LtHttpError(`${method} ${path}: ${reason}`, undefined, undefined, context(), { cause: err });
47
+ }
48
+ finally {
49
+ clearTimeout(timer);
50
+ }
51
+ const text = await res.text();
52
+ const parsed = (res.headers.get('content-type') ?? '').includes('json') && text ? JSON.parse(text) : text;
53
+ if (!res.ok)
54
+ throw new LtHttpError(`${method} ${path} → ${res.status}`, res.status, parsed, context());
55
+ return parsed;
56
+ }
57
+ }
58
+ exports.LtHttp = LtHttp;
59
+ /** Escalation verbs over HTTP, each run with the bearer token of the actor it runs as. */
60
+ class HttpLineEscalations {
61
+ constructor(http, tokenFor, claimMinutes = 24 * 60) {
62
+ this.http = http;
63
+ this.tokenFor = tokenFor;
64
+ this.claimMinutes = claimMinutes;
65
+ }
66
+ claim(as, id) {
67
+ return this.verb(as, 'POST', `/escalations/${id}/claim`, { durationMinutes: this.claimMinutes });
68
+ }
69
+ accumulate(as, id, itemKey, payload, metadata) {
70
+ return this.verb(as, 'POST', `/escalations/${id}/accumulate`, { itemKey, payload, ...(metadata ? { metadata } : {}) });
71
+ }
72
+ resolve(as, id, payload) {
73
+ return this.verb(as, 'POST', `/escalations/${id}/resolve`, { resolverPayload: payload });
74
+ }
75
+ async create(as, input) {
76
+ const row = await this.http.request('POST', '/escalations', this.tokenFor(as), input);
77
+ return { id: row.id };
78
+ }
79
+ async itemKeys(id) {
80
+ return (await this.items(id)).map((i) => i.itemKey);
81
+ }
82
+ async items(id) {
83
+ const view = await this.http.request('GET', `/escalations/${id}/items`, this.tokenFor(actions_1.CONTROLLER));
84
+ return view.items.map((i) => ({ itemKey: i.itemKey, payload: i.payload ?? {} }));
85
+ }
86
+ async get(id) {
87
+ if (!(0, uuid_1.isUuid)(id))
88
+ return null;
89
+ try {
90
+ return await this.http.request('GET', `/escalations/${encodeURIComponent(id)}`, this.tokenFor(actions_1.CONTROLLER));
91
+ }
92
+ catch (err) {
93
+ if (err instanceof LtHttpError && (err.status === 403 || err.status === 404))
94
+ return null;
95
+ throw err;
96
+ }
97
+ }
98
+ async listOpen(filter) {
99
+ return (0, escalations_1.readAllPages)(async (offset) => {
100
+ const query = new URLSearchParams({
101
+ ...filter, status: 'pending', limit: String(escalations_1.OPEN_PAGE), offset: String(offset), sort_by: 'created_at', order: 'asc',
102
+ });
103
+ const page = await this.http.request('GET', `/escalations?${query}`, this.tokenFor(actions_1.CONTROLLER));
104
+ return page.escalations;
105
+ });
106
+ }
107
+ async verb(as, method, path, body) {
108
+ try {
109
+ await this.http.request(method, path, this.tokenFor(as), body);
110
+ return { outcome: escalations_1.VERB_OUTCOME.DONE };
111
+ }
112
+ catch (err) {
113
+ const outcome = err instanceof LtHttpError && err.status !== undefined ? (0, escalations_1.verbOutcomeOf)(err.status) : null;
114
+ if (!outcome)
115
+ throw err;
116
+ return { outcome, detail: JSON.stringify(err.body), status: err.status };
117
+ }
118
+ }
119
+ }
120
+ exports.HttpLineEscalations = HttpLineEscalations;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pacing: a machine with a queue depth is sent no more jobs than that at once.
3
+ * A handoff beyond it waits in the machine's role queue, unclaimed, the way
4
+ * work waits for a busy person, and the oldest waiting row goes out when the
5
+ * machine has room. A machine's load is every job it has been sent or still
6
+ * holds: rows dispatched into it, rows whose units wait on it to move on, and
7
+ * units that arrived and have not left yet.
8
+ */
9
+ import { type LineDef } from '../../../lib/machines/line';
10
+ import { type Action } from './actions';
11
+ import { type GatewayState, type HandoffRow } from './state';
12
+ /** Dispatch arguments for a handoff row. */
13
+ export declare function dispatchArgs(row: HandoffRow, expiresAt: number): Record<string, any>;
14
+ export declare function dispatchAction(row: HandoffRow, machineId: string, dispatchTtlMs: number): Action;
15
+ /** A machine on a version the line does not allow is refused before any dispatch. */
16
+ export declare function softwareRefusal(state: GatewayState, line: LineDef, machineId: string): string | null;
17
+ /** A machine that requires an artifact is never sent a handoff without one: the job's, or one for every unit. */
18
+ export declare function artifactRefusal(line: LineDef, row: HandoffRow): string | null;
19
+ /** The jobs `machineId` has been sent or still holds. */
20
+ export declare function machineLoad(state: GatewayState, machineId: string): Set<string>;
21
+ export declare function hasRoom(state: GatewayState, line: LineDef, row: HandoffRow): boolean;
22
+ /** Two open handoffs may not name the same job at one machine: each row, subsets of a route's units included, needs its own. */
23
+ export declare function jobRefusal(state: GatewayState, row: HandoffRow): string | null;
24
+ /** The resolution refusing a handoff for the machine's software, a missing artifact or a job id in use; empty when it may go. */
25
+ export declare function refuseUnfit(state: GatewayState, line: LineDef, row: HandoffRow): Action[];
26
+ /** Send a handoff to its machine now, or refuse it for the machine's software or a missing artifact. */
27
+ export declare function admit(state: GatewayState, line: LineDef, row: HandoffRow, dispatchTtlMs: number): Action[];
28
+ /** Dispatch waiting rows, oldest first, while their machines have room; a full machine holds back its later rows too. */
29
+ export declare function planPacing(state: GatewayState, line: LineDef, dispatchTtlMs: number): Action[];
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * Pacing: a machine with a queue depth is sent no more jobs than that at once.
4
+ * A handoff beyond it waits in the machine's role queue, unclaimed, the way
5
+ * work waits for a busy person, and the oldest waiting row goes out when the
6
+ * machine has room. A machine's load is every job it has been sent or still
7
+ * holds: rows dispatched into it, rows whose units wait on it to move on, and
8
+ * units that arrived and have not left yet.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.dispatchArgs = dispatchArgs;
12
+ exports.dispatchAction = dispatchAction;
13
+ exports.softwareRefusal = softwareRefusal;
14
+ exports.artifactRefusal = artifactRefusal;
15
+ exports.machineLoad = machineLoad;
16
+ exports.hasRoom = hasRoom;
17
+ exports.jobRefusal = jobRefusal;
18
+ exports.refuseUnfit = refuseUnfit;
19
+ exports.admit = admit;
20
+ exports.planPacing = planPacing;
21
+ const contracts_1 = require("../contracts");
22
+ const protocol_1 = require("../../../lib/machines/protocol");
23
+ const line_1 = require("../../../lib/machines/line");
24
+ const actions_1 = require("./actions");
25
+ const jobs_1 = require("./jobs");
26
+ const state_1 = require("./state");
27
+ const registration_1 = require("./registration");
28
+ /** Dispatch arguments for a handoff row. */
29
+ function dispatchArgs(row, expiresAt) {
30
+ return {
31
+ jobId: row.jobId,
32
+ orderRef: row.orderRef,
33
+ recipe: { id: row.recipeId, version: row.recipeVersion },
34
+ units: row.arrivals ? row.units.map((u) => ({ ...u, arrival: row.arrivals?.[u.unitId] ?? null })) : row.units,
35
+ process: row.process,
36
+ from: row.fromMachine,
37
+ expiresAt: (0, protocol_1.isoTs)(expiresAt),
38
+ ...(row.artifactUrl ? { artifact: { url: row.artifactUrl, sha256: row.artifactSha256 } } : {}),
39
+ ...(row.parameters ? { parameters: row.parameters } : {}),
40
+ };
41
+ }
42
+ function dispatchAction(row, machineId, dispatchTtlMs) {
43
+ return {
44
+ kind: 'dispatch', rowId: row.id, machineId, cmdId: (0, contracts_1.dispatchCommandId)(row.id), args: dispatchArgs(row, Date.now() + dispatchTtlMs),
45
+ ...(row.dispatch === state_1.DISPATCH_STATE.ARRIVING ? { receivedOnAccept: true } : {}),
46
+ };
47
+ }
48
+ /** A machine on a version the line does not allow is refused before any dispatch. */
49
+ function softwareRefusal(state, line, machineId) {
50
+ const allowed = (0, line_1.machineById)(line, machineId).allowedSoftware;
51
+ const software = state.links.get(machineId)?.software;
52
+ if (!allowed || !software || allowed.includes(software))
53
+ return null;
54
+ return `${machineId} runs ${software}; allowed: ${allowed.join(', ')}`;
55
+ }
56
+ /** A machine that requires an artifact is never sent a handoff without one: the job's, or one for every unit. */
57
+ function artifactRefusal(line, row) {
58
+ if (!(0, line_1.machineById)(line, row.toMachine).requireArtifact || (row.artifactUrl && row.artifactSha256))
59
+ return null;
60
+ if (row.units.every((u) => u.artifact))
61
+ return null;
62
+ return `${row.toMachine} requires an artifact (url and sha256), for the job or for every unit; the row has none`;
63
+ }
64
+ const SENT = [state_1.DISPATCH_STATE.PENDING, state_1.DISPATCH_STATE.ACCEPTED];
65
+ const jobIdsOf = (state, jobId, row) => (0, jobs_1.dispatchJobs)(jobId, row.units, state.unitsPerDispatch).map((j) => j.jobId);
66
+ /** The jobs `machineId` has been sent or still holds. */
67
+ function machineLoad(state, machineId) {
68
+ const jobs = new Set(state.heldAt(machineId));
69
+ for (const r of state.open()) {
70
+ if (r.toMachine === machineId && SENT.includes(r.dispatch))
71
+ for (const id of jobIdsOf(state, r.jobId, r))
72
+ jobs.add(id);
73
+ if (r.fromMachine === machineId && r.senderJobId)
74
+ for (const id of jobIdsOf(state, r.senderJobId, r))
75
+ jobs.add(id);
76
+ }
77
+ return jobs;
78
+ }
79
+ /** Whether a row's jobs fit at its receiver; an idle machine always takes the next row, however large. */
80
+ function fits(line, row, load, state) {
81
+ const depth = (0, line_1.machineById)(line, row.toMachine).queueDepth;
82
+ return !depth || load.size === 0 || load.size + jobIdsOf(state, row.jobId, row).length <= depth;
83
+ }
84
+ function hasRoom(state, line, row) {
85
+ return fits(line, row, machineLoad(state, row.toMachine), state);
86
+ }
87
+ /** Two open handoffs may not name the same job at one machine: each row, subsets of a route's units included, needs its own. */
88
+ function jobRefusal(state, row) {
89
+ const other = state.open().find((r) => r.id !== row.id && r.toMachine === row.toMachine && r.jobId === row.jobId && r.dispatch !== state_1.DISPATCH_STATE.REFUSED);
90
+ return other ? `job ${row.jobId} at ${row.toMachine} already belongs to handoff ${other.id}` : null;
91
+ }
92
+ /** The resolution refusing a handoff for the machine's software, a missing artifact or a job id in use; empty when it may go. */
93
+ function refuseUnfit(state, line, row) {
94
+ const software = softwareRefusal(state, line, row.toMachine);
95
+ const other = software ?? artifactRefusal(line, row) ?? jobRefusal(state, row);
96
+ if (!other)
97
+ return [];
98
+ row.dispatch = state_1.DISPATCH_STATE.REFUSED;
99
+ const errorCode = software ? contracts_1.HANDOFF_ERROR.SOFTWARE_UNSUPPORTED : contracts_1.HANDOFF_ERROR.DISPATCH_REFUSED;
100
+ return [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: row.id, payload: { received: false, errorCode, detail: other } }];
101
+ }
102
+ /** Send a handoff to its machine now, or refuse it for the machine's software or a missing artifact. */
103
+ function admit(state, line, row, dispatchTtlMs) {
104
+ const refused = refuseUnfit(state, line, row);
105
+ if (refused.length)
106
+ return refused;
107
+ row.dispatch = state_1.DISPATCH_STATE.PENDING;
108
+ return [dispatchAction(row, row.toMachine, dispatchTtlMs)];
109
+ }
110
+ /** Dispatch waiting rows, oldest first, while their machines have room; a full machine holds back its later rows too. */
111
+ function planPacing(state, line, dispatchTtlMs) {
112
+ const actions = [];
113
+ const loads = new Map();
114
+ const full = new Set();
115
+ for (const row of state.open()) {
116
+ const m = row.toMachine;
117
+ // A station that has not registered its address yet keeps its rows waiting.
118
+ if (row.dispatch !== state_1.DISPATCH_STATE.WAITING || !m || full.has(m) || !(0, registration_1.reachable)(state, line, m))
119
+ continue;
120
+ if (!loads.has(m))
121
+ loads.set(m, machineLoad(state, m));
122
+ const load = loads.get(m);
123
+ if (!fits(line, row, load, state)) {
124
+ full.add(m);
125
+ continue;
126
+ }
127
+ const admitted = admit(state, line, row, dispatchTtlMs);
128
+ actions.push(...admitted);
129
+ if (admitted.some((a) => a.kind === 'dispatch'))
130
+ for (const id of jobIdsOf(state, row.jobId, row))
131
+ load.add(id);
132
+ }
133
+ return actions;
134
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Escalation events to gateway actions. A new handoff row for a machine is the
3
+ * cue to dispatch, exactly as a new row is the cue for a person to look at the
4
+ * dashboard. A cancelled or expired row withdraws its dispatch; a row commands
5
+ * only the machine whose role it is parked on, so the sender's job is the
6
+ * workflow's to stop (a machine-command row on the sender's role). An alarm
7
+ * row a person resolves acknowledges the alarm at the machine.
8
+ * A machine-command row is sent to its machine as the command it names.
9
+ */
10
+ import { type LineDef } from '../../../lib/machines/line';
11
+ import type { EscalationEvent } from '../../../lib/machines/transport';
12
+ import { type Action } from './actions';
13
+ import { type GatewayState } from './state';
14
+ import { type HandoffRowAdapter } from '../rows';
15
+ /** Whether an event names a row this line acts on; only then is the stored row read. */
16
+ export declare function concernsLine(state: GatewayState, line: LineDef, event: EscalationEvent, rows?: HandoffRowAdapter): boolean;
17
+ /** Whether a stored row's status agrees with the event's verb, so a stale or invented event does nothing. */
18
+ export declare function statusFits(verb: string, status: unknown): boolean;
19
+ export declare function planEscalationEvent(state: GatewayState, line: LineDef, event: EscalationEvent, dispatchTtlMs: number, rows?: HandoffRowAdapter): Action[];
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ /**
3
+ * Escalation events to gateway actions. A new handoff row for a machine is the
4
+ * cue to dispatch, exactly as a new row is the cue for a person to look at the
5
+ * dashboard. A cancelled or expired row withdraws its dispatch; a row commands
6
+ * only the machine whose role it is parked on, so the sender's job is the
7
+ * workflow's to stop (a machine-command row on the sender's role). An alarm
8
+ * row a person resolves acknowledges the alarm at the machine.
9
+ * A machine-command row is sent to its machine as the command it names.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.concernsLine = concernsLine;
13
+ exports.statusFits = statusFits;
14
+ exports.planEscalationEvent = planEscalationEvent;
15
+ const contracts_1 = require("../contracts");
16
+ const commands_1 = require("../commands");
17
+ const protocol_1 = require("../../../lib/machines/protocol");
18
+ const line_1 = require("../../../lib/machines/line");
19
+ const actions_1 = require("./actions");
20
+ const plan_shared_1 = require("./plan-shared");
21
+ const jobs_1 = require("./jobs");
22
+ const pacing_1 = require("./pacing");
23
+ const trace_1 = require("./trace");
24
+ const state_1 = require("./state");
25
+ const rows_1 = require("../rows");
26
+ const registration_1 = require("./registration");
27
+ const VERB = { CREATED: 'created', CLAIMED: 'claimed', RESOLVED: 'resolved', CANCELLED: 'cancelled', EXPIRED: 'expired' };
28
+ /** The stored status a row must have for each verb the gateway acts on. */
29
+ const STATUS_FOR_VERB = {
30
+ [VERB.CREATED]: 'pending', [VERB.CLAIMED]: 'pending', [VERB.RESOLVED]: 'resolved', [VERB.CANCELLED]: 'cancelled', [VERB.EXPIRED]: 'expired',
31
+ };
32
+ const LINE_ROW_TYPES = [contracts_1.ROW_TYPE.ALARM, contracts_1.ROW_TYPE.MACHINE_OFFLINE, contracts_1.ROW_TYPE.MACHINE_QUIET, contracts_1.ROW_TYPE.MACHINE_COMMAND, contracts_1.ROW_TYPE.MACHINE_REGISTRATION];
33
+ function handoffRow(id, role, facets, metadata) {
34
+ return { ...facets, id, role, dispatch: state_1.DISPATCH_STATE.NONE, trace: (0, trace_1.readTrace)(metadata) };
35
+ }
36
+ /** Rows for another line belong to that line's gateway. */
37
+ function onThisLine(line, at) {
38
+ return at.siteId === line.siteId && at.lineId === line.lineId;
39
+ }
40
+ /** Whether an event names a row this line acts on; only then is the stored row read. */
41
+ function concernsLine(state, line, event, rows = rows_1.standardRows) {
42
+ if (!STATUS_FOR_VERB[event.verb])
43
+ return false;
44
+ if (state.get(event.escalationId))
45
+ return true;
46
+ const facets = rows.toHandoff({ ...event.data, id: event.escalationId, role: event.role });
47
+ if (facets)
48
+ return onThisLine(line, facets);
49
+ return LINE_ROW_TYPES.includes(event.data?.type) && onThisLine(line, event.data?.metadata ?? {});
50
+ }
51
+ /** Whether a stored row's status agrees with the event's verb, so a stale or invented event does nothing. */
52
+ function statusFits(verb, status) {
53
+ return STATUS_FOR_VERB[verb] === status;
54
+ }
55
+ /** A handoff into a machine is parked on that machine's role; one to a person, on one of the line's receiver roles. */
56
+ function handoffRoleFits(line, facets, role) {
57
+ if (onLine(line, facets.toMachine))
58
+ return role === (0, line_1.machineRole)((0, line_1.machineById)(line, facets.toMachine));
59
+ return (0, line_1.receiverRoles)(line).includes(role);
60
+ }
61
+ function planEscalationEvent(state, line, event, dispatchTtlMs, rows = rows_1.standardRows) {
62
+ const view = { ...event.data, id: event.escalationId, role: event.role };
63
+ const facets = rows.toHandoff(view);
64
+ if (facets)
65
+ return onThisLine(line, facets) && handoffRoleFits(line, facets, event.role) ? planHandoff(state, line, event, facets, dispatchTtlMs) : [];
66
+ if (state.get(event.escalationId))
67
+ return planHandoff(state, line, event, null, dispatchTtlMs);
68
+ const type = event.data?.type;
69
+ if (!onThisLine(line, event.data?.metadata ?? {}))
70
+ return [];
71
+ if (type === contracts_1.ROW_TYPE.ALARM)
72
+ return planAlarmRow(state, event);
73
+ if (type === contracts_1.ROW_TYPE.MACHINE_OFFLINE || type === contracts_1.ROW_TYPE.MACHINE_QUIET)
74
+ return planLinkRow(state, event);
75
+ if (type === contracts_1.ROW_TYPE.MACHINE_COMMAND)
76
+ return planCommandRow(state, line, event);
77
+ if (type === contracts_1.ROW_TYPE.MACHINE_REGISTRATION)
78
+ return event.role === (0, line_1.operatorRole)(line) ? (0, registration_1.planRegistrationRow)(state, line, event) : [];
79
+ return [];
80
+ }
81
+ /** Why the gateway will not send a command row's command; null when it will. */
82
+ function commandRefusal(state, line, c, role) {
83
+ if (!onLine(line, c.machineId))
84
+ return `${c.machineId} is not on ${line.siteId}/${line.lineId}`;
85
+ const own = (0, line_1.machineRole)((0, line_1.machineById)(line, c.machineId));
86
+ if (role !== own)
87
+ return `commands for ${c.machineId} are parked on ${own}, not ${role}`;
88
+ if (!protocol_1.CONTROL_OPS.includes(c.op))
89
+ return `${c.op} is not a command a row may ask for; one of ${protocol_1.CONTROL_OPS.join(', ')}`;
90
+ if (c.op === protocol_1.OP.CANCEL && typeof c.args.jobId !== 'string')
91
+ return `${protocol_1.OP.CANCEL} needs args.jobId, the job to withdraw or stop`;
92
+ if (!(0, registration_1.reachable)(state, line, c.machineId))
93
+ return `${c.machineId} has not registered an address yet`;
94
+ return null;
95
+ }
96
+ function planCommandRow(state, line, event) {
97
+ const id = event.escalationId;
98
+ if (event.verb === VERB.RESOLVED || event.verb === VERB.CANCELLED || event.verb === VERB.EXPIRED) {
99
+ state.endCommand(id);
100
+ return [];
101
+ }
102
+ if (event.verb !== VERB.CREATED && event.verb !== VERB.CLAIMED)
103
+ return [];
104
+ const c = (0, commands_1.readCommandFacets)(event.data?.metadata);
105
+ if (!c || !state.trackCommand(id))
106
+ return [];
107
+ const refusal = commandRefusal(state, line, c, event.role);
108
+ if (refusal) {
109
+ return [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: id, payload: { accepted: false, machineId: c.machineId, op: c.op, status: contracts_1.COMMAND_STATUS.UNSUPPORTED, reason: refusal } }];
110
+ }
111
+ return [{ kind: 'control', rowId: id, machineId: c.machineId, op: c.op, cmdId: (0, contracts_1.controlCommandId)(id), args: c.args }];
112
+ }
113
+ function onLine(line, machineId) {
114
+ return !!machineId && line.machines.some((m) => m.machineId === machineId);
115
+ }
116
+ function heldUnder(state, machineId, unitId, senderJobId) {
117
+ const held = state.heldJob(machineId, unitId);
118
+ return !!held && (!senderJobId || (0, jobs_1.jobOwns)(senderJobId, held));
119
+ }
120
+ function planHandoff(state, line, event, facets, dispatchTtlMs) {
121
+ const id = event.escalationId;
122
+ if (event.verb === VERB.CREATED || event.verb === VERB.CLAIMED) {
123
+ if (state.isClosed(id) || !facets)
124
+ return [];
125
+ state.index(handoffRow(id, event.role, facets, event.data?.metadata));
126
+ const row = state.get(id);
127
+ // The units now wait on the sender as this row's sender job, when the sender holds them under it.
128
+ if (row.fromMachine)
129
+ for (const u of row.units)
130
+ if (heldUnder(state, row.fromMachine, u.unitId, row.senderJobId))
131
+ state.letGo(row.fromMachine, u.unitId);
132
+ if (!onLine(line, row.toMachine) || row.dispatch !== state_1.DISPATCH_STATE.NONE)
133
+ return [];
134
+ // Read back already claimed: the machine accepted it before this gateway started.
135
+ if (event.data?.assigned_to) {
136
+ row.dispatch = state_1.DISPATCH_STATE.ACCEPTED;
137
+ return [];
138
+ }
139
+ if (state.dispatchOnArrival) {
140
+ const refused = (0, pacing_1.refuseUnfit)(state, line, row);
141
+ if (!refused.length)
142
+ row.dispatch = state_1.DISPATCH_STATE.ARRIVING;
143
+ return refused;
144
+ }
145
+ const refused = !!(0, pacing_1.softwareRefusal)(state, line, row.toMachine) || !!(0, pacing_1.jobRefusal)(state, row);
146
+ if (state.holdDispatch || (!refused && (!(0, pacing_1.hasRoom)(state, line, row) || !(0, registration_1.reachable)(state, line, row.toMachine)))) {
147
+ row.dispatch = state_1.DISPATCH_STATE.WAITING;
148
+ return [];
149
+ }
150
+ return (0, pacing_1.admit)(state, line, row, dispatchTtlMs);
151
+ }
152
+ if (event.verb === VERB.CANCELLED || event.verb === VERB.EXPIRED) {
153
+ const row = state.close(id, event.verb === VERB.CANCELLED ? state_1.ROW_END.CANCELLED : state_1.ROW_END.EXPIRED);
154
+ return row ? (0, plan_shared_1.cancelDispatch)(state, row) : [];
155
+ }
156
+ if (event.verb === VERB.RESOLVED)
157
+ state.close(id);
158
+ return [];
159
+ }
160
+ function planAlarmRow(state, event) {
161
+ const md = event.data.metadata ?? {};
162
+ const track = state.alarms.get(state.alarmKey(md.machineId, md.alarmId));
163
+ if (event.verb === VERB.CREATED) {
164
+ // A row for an earlier raise of the same alarm id is not this raise's row.
165
+ if (track && !track.rowId && (!track.raisedAt || !md.raisedAt || track.raisedAt === md.raisedAt))
166
+ track.rowId = event.escalationId;
167
+ return [];
168
+ }
169
+ // Only the row of the raise still tracked speaks for it; an older row ending acknowledges nothing.
170
+ if (!track || track.rowId !== event.escalationId || (event.verb !== VERB.RESOLVED && event.verb !== VERB.CANCELLED))
171
+ return [];
172
+ track.open = false;
173
+ if (event.verb !== VERB.RESOLVED)
174
+ return [];
175
+ // Still tracked means the machine has not cleared it: a person acknowledged it.
176
+ return [{
177
+ kind: 'command', machineId: track.machineId, op: protocol_1.OP.ACKNOWLEDGE_ALARM, cmdId: `${event.escalationId}:ack`,
178
+ args: { alarmId: track.alarmId, by: event.data.resolved_by ?? 'line-operator' },
179
+ }];
180
+ }
181
+ function planLinkRow(state, event) {
182
+ const machineId = event.data.metadata?.machineId;
183
+ const link = machineId ? state.links.get(machineId) : undefined;
184
+ if (!link)
185
+ return [];
186
+ const quiet = event.data.type === contracts_1.ROW_TYPE.MACHINE_QUIET;
187
+ const ended = event.verb === VERB.RESOLVED || event.verb === VERB.CANCELLED;
188
+ if (quiet) {
189
+ if (event.verb === VERB.CREATED)
190
+ link.quietRowId = event.escalationId;
191
+ if (ended && link.quietRowId === event.escalationId)
192
+ link.quietRowId = undefined;
193
+ }
194
+ else {
195
+ if (event.verb === VERB.CREATED)
196
+ link.rowId = event.escalationId;
197
+ if (ended && link.rowId === event.escalationId)
198
+ link.rowId = undefined;
199
+ }
200
+ return [];
201
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Machine messages to gateway actions: the escalation verbs a person would
3
+ * perform, performed for the machine. Each unit's arrival and the sender's
4
+ * finished record accumulate on the handoff row, and the last expected item
5
+ * resolves it. A failure on either side resolves it with an error. Alarms that
6
+ * stop work and machines that go dark or quiet become rows for people. A
7
+ * restarted machine gets back the dispatches it lost. Everything else stays
8
+ * in the stream.
9
+ *
10
+ * A message whose row does not exist yet is nak'd and arrives again; one whose
11
+ * row already closed is acked, so a replay never resolves anything twice.
12
+ */
13
+ import { type Envelope, type ParsedTopic } from '../../../lib/machines/protocol';
14
+ import { type Plan } from './actions';
15
+ import { type GatewayState } from './state';
16
+ export declare function planMachineMessage(state: GatewayState, parsed: ParsedTopic, env: Envelope | null, dispatchTtlMs: number): Plan;