@hotmeshio/long-tail 0.25.4 → 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 (139) 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/scripts/machine-line.d.ts +33 -0
  51. package/build/scripts/machine-line.js +301 -0
  52. package/build/services/machines/commands.d.ts +29 -0
  53. package/build/services/machines/commands.js +37 -0
  54. package/build/services/machines/config.d.ts +43 -0
  55. package/build/services/machines/config.js +84 -0
  56. package/build/services/machines/contracts.d.ts +193 -0
  57. package/build/services/machines/contracts.js +115 -0
  58. package/build/services/machines/forms.d.ts +55 -0
  59. package/build/services/machines/forms.js +37 -0
  60. package/build/services/machines/gateway/actions.d.ts +111 -0
  61. package/build/services/machines/gateway/actions.js +8 -0
  62. package/build/services/machines/gateway/address-book.d.ts +16 -0
  63. package/build/services/machines/gateway/address-book.js +24 -0
  64. package/build/services/machines/gateway/admission.d.ts +35 -0
  65. package/build/services/machines/gateway/admission.js +66 -0
  66. package/build/services/machines/gateway/api-escalations.d.ts +23 -0
  67. package/build/services/machines/gateway/api-escalations.js +101 -0
  68. package/build/services/machines/gateway/dispatcher.d.ts +63 -0
  69. package/build/services/machines/gateway/dispatcher.js +157 -0
  70. package/build/services/machines/gateway/escalations.d.ts +51 -0
  71. package/build/services/machines/gateway/escalations.js +40 -0
  72. package/build/services/machines/gateway/executor.d.ts +55 -0
  73. package/build/services/machines/gateway/executor.js +195 -0
  74. package/build/services/machines/gateway/gateway.d.ts +82 -0
  75. package/build/services/machines/gateway/gateway.js +247 -0
  76. package/build/services/machines/gateway/index.d.ts +10 -0
  77. package/build/services/machines/gateway/index.js +26 -0
  78. package/build/services/machines/gateway/jobs.d.ts +16 -0
  79. package/build/services/machines/gateway/jobs.js +24 -0
  80. package/build/services/machines/gateway/log.d.ts +41 -0
  81. package/build/services/machines/gateway/log.js +42 -0
  82. package/build/services/machines/gateway/lt-http.d.ts +51 -0
  83. package/build/services/machines/gateway/lt-http.js +120 -0
  84. package/build/services/machines/gateway/pacing.d.ts +29 -0
  85. package/build/services/machines/gateway/pacing.js +134 -0
  86. package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
  87. package/build/services/machines/gateway/plan-escalation.js +201 -0
  88. package/build/services/machines/gateway/plan-machine.d.ts +16 -0
  89. package/build/services/machines/gateway/plan-machine.js +242 -0
  90. package/build/services/machines/gateway/plan-shared.d.ts +18 -0
  91. package/build/services/machines/gateway/plan-shared.js +36 -0
  92. package/build/services/machines/gateway/provision.d.ts +22 -0
  93. package/build/services/machines/gateway/provision.js +79 -0
  94. package/build/services/machines/gateway/receipts.d.ts +33 -0
  95. package/build/services/machines/gateway/receipts.js +65 -0
  96. package/build/services/machines/gateway/reconcile.d.ts +33 -0
  97. package/build/services/machines/gateway/reconcile.js +86 -0
  98. package/build/services/machines/gateway/registration.d.ts +38 -0
  99. package/build/services/machines/gateway/registration.js +121 -0
  100. package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
  101. package/build/services/machines/gateway/reply-waiter.js +65 -0
  102. package/build/services/machines/gateway/state.d.ts +125 -0
  103. package/build/services/machines/gateway/state.js +228 -0
  104. package/build/services/machines/gateway/trace.d.ts +16 -0
  105. package/build/services/machines/gateway/trace.js +43 -0
  106. package/build/services/machines/identities.d.ts +25 -0
  107. package/build/services/machines/identities.js +77 -0
  108. package/build/services/machines/index.d.ts +11 -0
  109. package/build/services/machines/index.js +27 -0
  110. package/build/services/machines/leadership.d.ts +22 -0
  111. package/build/services/machines/leadership.js +75 -0
  112. package/build/services/machines/roles.d.ts +13 -0
  113. package/build/services/machines/roles.js +62 -0
  114. package/build/services/machines/rows.d.ts +30 -0
  115. package/build/services/machines/rows.js +56 -0
  116. package/build/services/machines/select.d.ts +18 -0
  117. package/build/services/machines/select.js +63 -0
  118. package/build/services/machines/surface.d.ts +33 -0
  119. package/build/services/machines/surface.js +98 -0
  120. package/build/services/machines/testing/fake-long-tail.d.ts +78 -0
  121. package/build/services/machines/testing/fake-long-tail.js +155 -0
  122. package/build/services/machines/testing/index.d.ts +3 -0
  123. package/build/services/machines/testing/index.js +19 -0
  124. package/build/services/machines/testing/recording-log.d.ts +14 -0
  125. package/build/services/machines/testing/recording-log.js +22 -0
  126. package/build/services/machines/testing/wait.d.ts +3 -0
  127. package/build/services/machines/testing/wait.js +17 -0
  128. package/build/services/mcp/db-server/schemas.d.ts +2 -2
  129. package/build/start/index.js +13 -0
  130. package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
  131. package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
  132. package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
  133. package/build/tsconfig.tsbuildinfo +1 -1
  134. package/build/types/index.d.ts +2 -1
  135. package/build/types/machines.d.ts +166 -0
  136. package/build/types/machines.js +7 -0
  137. package/build/types/startup.d.ts +23 -0
  138. package/docs/machines.md +293 -0
  139. package/package.json +11 -1
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * Identities inside Long Tail: each machine is a bot account holding its
4
+ * station roles, so rows it claims and resolves name the machine. Bots are
5
+ * found by name and given any role they are missing; nothing is removed. A
6
+ * person's account under a bot's name stops the line from starting.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.BotAddressBook = exports.ADDRESS_PROPERTY = void 0;
10
+ exports.ensureLineBots = ensureLineBots;
11
+ const iam_1 = require("../iam");
12
+ const user_1 = require("../user");
13
+ const actions_1 = require("./gateway/actions");
14
+ const provision_1 = require("./gateway/provision");
15
+ /** The user property on a station's bot account that holds the address its machine registered. */
16
+ exports.ADDRESS_PROPERTY = 'machineAddress';
17
+ const MEMBER = 'member';
18
+ const BOT = 'bot';
19
+ /** Make sure every bot the line needs exists with its roles; returns actor → user id. */
20
+ async function ensureLineBots(line) {
21
+ const ids = new Map();
22
+ for (const identity of (0, provision_1.lineIdentities)(line)) {
23
+ const existing = await (0, user_1.getUserByExternalId)(identity.botName);
24
+ if (existing && existing.account_type !== BOT) {
25
+ throw new Error(`"${identity.botName}" belongs to a person's account; the gateway acts only as bot accounts`);
26
+ }
27
+ if (!existing) {
28
+ const bot = await (0, iam_1.createBot)({
29
+ name: identity.botName,
30
+ description: `Machine line ${line.siteId}/${line.lineId}: ${identity.actor}`,
31
+ roles: identity.roles.map((role) => ({ role, type: MEMBER })),
32
+ });
33
+ ids.set(identity.actor, bot.id);
34
+ continue;
35
+ }
36
+ const held = new Set((await (0, iam_1.getBotRoles)(existing.id)).map((r) => r.role));
37
+ for (const role of identity.roles.filter((r) => !held.has(r)))
38
+ await (0, iam_1.addBotRole)(existing.id, role, MEMBER);
39
+ ids.set(identity.actor, existing.id);
40
+ }
41
+ return ids;
42
+ }
43
+ /**
44
+ * Learned addresses kept on each station's bot account, so a replica that
45
+ * takes the line over sends to the machines the line already knows. An address
46
+ * belongs to one station: binding it to a station clears it from any other.
47
+ */
48
+ class BotAddressBook {
49
+ constructor(bots) {
50
+ this.bots = bots;
51
+ }
52
+ async load() {
53
+ const bound = new Map();
54
+ for (const [machineId, address] of await this.read())
55
+ if (address)
56
+ bound.set(machineId, address);
57
+ return bound;
58
+ }
59
+ async bind(machineId, address) {
60
+ const id = this.bots.get(machineId);
61
+ if (!id)
62
+ throw new Error(`no bot for station "${machineId}"`);
63
+ for (const [other, held] of await this.read()) {
64
+ if (other !== machineId && held === address)
65
+ await (0, user_1.patchUserProperties)(this.bots.get(other), { remove: [exports.ADDRESS_PROPERTY] });
66
+ }
67
+ await (0, user_1.patchUserProperties)(id, { set: { [exports.ADDRESS_PROPERTY]: address } });
68
+ }
69
+ async read() {
70
+ const stations = [...this.bots.entries()].filter(([actor]) => actor !== actions_1.CONTROLLER);
71
+ return Promise.all(stations.map(async ([machineId, id]) => {
72
+ const address = (await (0, user_1.getUser)(id))?.metadata?.[exports.ADDRESS_PROPERTY];
73
+ return [machineId, typeof address === 'string' ? address : undefined];
74
+ }));
75
+ }
76
+ }
77
+ exports.BotAddressBook = BotAddressBook;
@@ -0,0 +1,11 @@
1
+ export * from './config';
2
+ export * from './contracts';
3
+ export * from './commands';
4
+ export * from './forms';
5
+ export * from './select';
6
+ export * from './gateway';
7
+ export * from './roles';
8
+ export * from './identities';
9
+ export * from './surface';
10
+ export * from './rows';
11
+ export * from '../../lib/machines';
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./config"), exports);
18
+ __exportStar(require("./contracts"), exports);
19
+ __exportStar(require("./commands"), exports);
20
+ __exportStar(require("./forms"), exports);
21
+ __exportStar(require("./select"), exports);
22
+ __exportStar(require("./gateway"), exports);
23
+ __exportStar(require("./roles"), exports);
24
+ __exportStar(require("./identities"), exports);
25
+ __exportStar(require("./surface"), exports);
26
+ __exportStar(require("./rows"), exports);
27
+ __exportStar(require("../../lib/machines"), exports);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * One gateway per line across every replica. Each line contends for a
3
+ * Postgres advisory lock on a dedicated connection; `pg_advisory_lock`
4
+ * waits on the server, so standby replicas hold no timers and the next one
5
+ * takes over the moment the leader's session ends.
6
+ */
7
+ export declare class LineLeadership {
8
+ readonly key: string;
9
+ private client?;
10
+ private stopped;
11
+ private leading;
12
+ constructor(key: string);
13
+ /**
14
+ * Wait for the lock, then call `lead`. When leading fails or the
15
+ * connection drops, the lock is released, `lost` runs, and the line
16
+ * contends again after a pause.
17
+ */
18
+ run(lead: () => Promise<void>, lost: (err: Error) => Promise<void>): Promise<void>;
19
+ private leadOnce;
20
+ /** A leader unlocks; a replica still waiting drops its connection, which ends the wait. */
21
+ stop(): Promise<void>;
22
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ /**
3
+ * One gateway per line across every replica. Each line contends for a
4
+ * Postgres advisory lock on a dedicated connection; `pg_advisory_lock`
5
+ * waits on the server, so standby replicas hold no timers and the next one
6
+ * takes over the moment the leader's session ends.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.LineLeadership = void 0;
10
+ const db_1 = require("../../lib/db");
11
+ const LOCK_SQL = 'SELECT pg_advisory_lock(hashtext($1))';
12
+ const RETRY_MS = 5_000;
13
+ const UNLOCK_SQL = 'SELECT pg_advisory_unlock(hashtext($1))';
14
+ class LineLeadership {
15
+ constructor(key) {
16
+ this.key = key;
17
+ this.stopped = false;
18
+ this.leading = false;
19
+ }
20
+ /**
21
+ * Wait for the lock, then call `lead`. When leading fails or the
22
+ * connection drops, the lock is released, `lost` runs, and the line
23
+ * contends again after a pause.
24
+ */
25
+ async run(lead, lost) {
26
+ while (!this.stopped) {
27
+ const err = await this.leadOnce(lead);
28
+ if (this.stopped)
29
+ return;
30
+ await lost(err);
31
+ await new Promise((r) => setTimeout(r, RETRY_MS));
32
+ }
33
+ }
34
+ async leadOnce(lead) {
35
+ const client = await (0, db_1.getPool)().connect();
36
+ this.client = client;
37
+ const dropped = new Promise((resolve) => client.once('error', resolve));
38
+ try {
39
+ await client.query(LOCK_SQL, [this.key]);
40
+ this.leading = true;
41
+ await lead();
42
+ return await dropped;
43
+ }
44
+ catch (err) {
45
+ return err;
46
+ }
47
+ finally {
48
+ this.leading = false;
49
+ if (!this.stopped) {
50
+ this.client = undefined;
51
+ client.release(true);
52
+ }
53
+ }
54
+ }
55
+ /** A leader unlocks; a replica still waiting drops its connection, which ends the wait. */
56
+ async stop() {
57
+ this.stopped = true;
58
+ const client = this.client;
59
+ this.client = undefined;
60
+ if (!client)
61
+ return;
62
+ if (!this.leading) {
63
+ client.release(true);
64
+ return;
65
+ }
66
+ try {
67
+ await client.query(UNLOCK_SQL, [this.key]);
68
+ client.release();
69
+ }
70
+ catch {
71
+ client.release(true);
72
+ }
73
+ }
74
+ }
75
+ exports.LineLeadership = LineLeadership;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The roles a line needs, as code-owned role declarations: one station role
3
+ * per process (a machine resolves the handoffs into it), the exit role (a
4
+ * person receives finished units) and the operator role (a person decides
5
+ * failed handoffs and closes alarm and machine rows). Each role owns its
6
+ * form; only the operator role enforces it on the server, because handoff
7
+ * rows also collect machine records the gateway already validates.
8
+ */
9
+ import type { LTRoleConfig } from '../../types/startup';
10
+ import { type LineDef } from '../../lib/machines/line';
11
+ export declare function machineRoleConfigs(line: LineDef): LTRoleConfig[];
12
+ /** The host's declarations first; line roles only where the host declared none. */
13
+ export declare function withMachineRoles(declared: LTRoleConfig[] | undefined, lines: LineDef[]): LTRoleConfig[];
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /**
3
+ * The roles a line needs, as code-owned role declarations: one station role
4
+ * per process (a machine resolves the handoffs into it), the exit role (a
5
+ * person receives finished units) and the operator role (a person decides
6
+ * failed handoffs and closes alarm and machine rows). Each role owns its
7
+ * form; only the operator role enforces it on the server, because handoff
8
+ * rows also collect machine records the gateway already validates.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.machineRoleConfigs = machineRoleConfigs;
12
+ exports.withMachineRoles = withMachineRoles;
13
+ const line_1 = require("../../lib/machines/line");
14
+ const forms_1 = require("./forms");
15
+ const BOARD = { sla_minutes: 10, target_per_hour: 60, priority_threshold_minutes: 10, ops_visible: true };
16
+ function machineRoleConfigs(line) {
17
+ const stations = [...new Map(line.machines.map((m) => [(0, line_1.machineRole)(m), m])).values()];
18
+ const chain = stations.map((m, i) => ({
19
+ role: (0, line_1.machineRole)(m),
20
+ title: `Line: ${m.process}`,
21
+ description: `Units handed to ${m.process}; the machine claims and resolves each handoff.`,
22
+ ...(i > 0 ? { parent_role: (0, line_1.machineRole)(stations[i - 1]) } : {}),
23
+ form_schema: forms_1.HANDOFF_FORM_SCHEMA,
24
+ enforce_schema: false,
25
+ ...BOARD,
26
+ }));
27
+ return [
28
+ ...chain,
29
+ {
30
+ role: (0, line_1.exitRole)(line),
31
+ title: 'Line exit',
32
+ description: 'Finished units leaving the line; a person confirms they are in hand.',
33
+ ...(chain.length ? { parent_role: chain[chain.length - 1].role } : {}),
34
+ upstream_roles: [(0, line_1.operatorRole)(line)],
35
+ form_schema: forms_1.HANDOFF_FORM_SCHEMA,
36
+ enforce_schema: false,
37
+ ...BOARD,
38
+ },
39
+ {
40
+ role: (0, line_1.operatorRole)(line),
41
+ title: 'Line operator',
42
+ description: 'Failed handoffs, stopping alarms, and machines that went dark or quiet.',
43
+ form_schema: forms_1.OPERATOR_FORM_SCHEMA,
44
+ enforce_schema: true,
45
+ ...BOARD,
46
+ },
47
+ ];
48
+ }
49
+ /** The host's declarations first; line roles only where the host declared none. */
50
+ function withMachineRoles(declared = [], lines) {
51
+ const out = [...declared];
52
+ const seen = new Set(out.map((r) => r.role));
53
+ for (const line of lines) {
54
+ for (const role of machineRoleConfigs(line)) {
55
+ if (seen.has(role.role))
56
+ continue;
57
+ seen.add(role.role);
58
+ out.push(role);
59
+ }
60
+ }
61
+ return out;
62
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Which escalation rows are machine handoffs, and what each one asks for.
3
+ * The workflow parks an ordinary row; this adapter reads it into the task
4
+ * the gateway dispatches. The standard adapter reads the handoff facets
5
+ * (contracts.ts); a host whose rows carry its own facets supplies its own,
6
+ * so neither the workflow nor its rows change to be resolved by a machine.
7
+ */
8
+ import type { LineDef } from '../../lib/machines/line';
9
+ import { type HandoffFacets } from './contracts';
10
+ /** The parts of an escalation row the adapter reads. */
11
+ export interface EscalationRowView {
12
+ id: string;
13
+ role: string;
14
+ type?: string;
15
+ subtype?: string;
16
+ metadata?: Record<string, any>;
17
+ }
18
+ /** Open rows to read when the gateway starts leading a line. */
19
+ export interface OpenRowQuery {
20
+ type?: string;
21
+ role?: string;
22
+ }
23
+ export interface HandoffRowAdapter {
24
+ openRows(line: LineDef): OpenRowQuery[];
25
+ /** The handoff a row asks for, or null when it is not a machine handoff. */
26
+ toHandoff(row: EscalationRowView): HandoffFacets | null;
27
+ }
28
+ /** Read standard handoff facets; null when the line, job or units are missing. */
29
+ export declare function readHandoffFacets(md?: Record<string, any>): HandoffFacets | null;
30
+ export declare const standardRows: HandoffRowAdapter;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * Which escalation rows are machine handoffs, and what each one asks for.
4
+ * The workflow parks an ordinary row; this adapter reads it into the task
5
+ * the gateway dispatches. The standard adapter reads the handoff facets
6
+ * (contracts.ts); a host whose rows carry its own facets supplies its own,
7
+ * so neither the workflow nor its rows change to be resolved by a machine.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.standardRows = void 0;
11
+ exports.readHandoffFacets = readHandoffFacets;
12
+ const contracts_1 = require("./contracts");
13
+ const str = (v) => (v === undefined || v === null || v === '' ? null : String(v));
14
+ /** A plain object of parameters; anything else is not parameters. */
15
+ function paramsOf(v) {
16
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
17
+ }
18
+ function artifactOf(v) {
19
+ return v && typeof v.url === 'string' && v.url && typeof v.sha256 === 'string' && v.sha256 ? { url: v.url, sha256: v.sha256 } : undefined;
20
+ }
21
+ function unitsOf(v) {
22
+ if (!Array.isArray(v))
23
+ return [];
24
+ return v.filter((u) => u && u.unitId).map((u) => {
25
+ const artifact = artifactOf(u.artifact);
26
+ const parameters = paramsOf(u.parameters);
27
+ return { unitId: String(u.unitId), carrierId: str(u.carrierId), ...(artifact ? { artifact } : {}), ...(parameters ? { parameters } : {}) };
28
+ });
29
+ }
30
+ /** Read standard handoff facets; null when the line, job or units are missing. */
31
+ function readHandoffFacets(md = {}) {
32
+ const units = unitsOf(md.units);
33
+ if (!md.siteId || !md.lineId || !md.jobId || !units.length)
34
+ return null;
35
+ return {
36
+ siteId: String(md.siteId),
37
+ lineId: String(md.lineId),
38
+ units,
39
+ jobId: String(md.jobId),
40
+ senderJobId: str(md.senderJobId),
41
+ orderRef: String(md.orderRef ?? ''),
42
+ fromMachine: str(md.fromMachine),
43
+ toMachine: str(md.toMachine),
44
+ recipeId: str(md.recipeId),
45
+ recipeVersion: str(md.recipeVersion),
46
+ process: str(md.process),
47
+ artifactUrl: str(md.artifactUrl),
48
+ artifactSha256: str(md.artifactSha256),
49
+ attempt: Number(md.attempt ?? 1),
50
+ ...(paramsOf(md.parameters) ? { parameters: paramsOf(md.parameters) } : {}),
51
+ };
52
+ }
53
+ exports.standardRows = {
54
+ openRows: () => [{ type: contracts_1.ROW_TYPE.HANDOFF }],
55
+ toHandoff: (row) => (row.type === contracts_1.ROW_TYPE.HANDOFF ? readHandoffFacets(row.metadata) : null),
56
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Transport selection from config. The memory transport only exists inside
3
+ * one process (tests build it directly), so asking for it here throws, as
4
+ * does any mode missing what it needs.
5
+ */
6
+ import { type MachineLineConfig } from './config';
7
+ import { type LineDef } from '../../lib/machines/line';
8
+ import { type LineCodec } from '../../lib/machines/protocol';
9
+ import type { CallbackEventAdapter } from '../../lib/events/callback';
10
+ import type { EscalationFeed, MachineBus, MachineLink } from '../../lib/machines/transport/delivery';
11
+ /** The codec for a line when the host supplies none: the line protocol under MACHINE_TOPIC_ROOT. */
12
+ export declare function defaultCodec(config: MachineLineConfig, line?: LineDef): LineCodec;
13
+ /** The gateway's view of every machine on the line. */
14
+ export declare function createMachineBus(config: MachineLineConfig, line: LineDef, codec?: LineCodec): MachineBus;
15
+ /** Long Tail's escalation events for the gateway; `in-process` needs the running instance's callback adapter. */
16
+ export declare function createEscalationFeed(config: MachineLineConfig, line: LineDef, callbacks?: CallbackEventAdapter): EscalationFeed;
17
+ /** One MQTT link per simulated machine; machines speak MQTT in every networked mode. */
18
+ export declare function createLinkFactory(config: MachineLineConfig): () => MachineLink;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Transport selection from config. The memory transport only exists inside
4
+ * one process (tests build it directly), so asking for it here throws, as
5
+ * does any mode missing what it needs.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.defaultCodec = defaultCodec;
9
+ exports.createMachineBus = createMachineBus;
10
+ exports.createEscalationFeed = createEscalationFeed;
11
+ exports.createLinkFactory = createLinkFactory;
12
+ const config_1 = require("./config");
13
+ const line_1 = require("../../lib/machines/line");
14
+ const protocol_1 = require("../../lib/machines/protocol");
15
+ const mqtt_bus_1 = require("../../lib/machines/transport/mqtt-bus");
16
+ const mqtt_link_1 = require("../../lib/machines/transport/mqtt-link");
17
+ const nats_bus_1 = require("../../lib/machines/transport/nats-bus");
18
+ const callback_feed_1 = require("../../lib/machines/transport/callback-feed");
19
+ const nats_feed_1 = require("../../lib/machines/transport/nats-feed");
20
+ const IN_PROCESS_ONLY = 'lives inside one process; build MemoryMachineBus and MemoryEscalationFeed directly';
21
+ function mqttOf(config) {
22
+ if (!config.mqtt)
23
+ throw new Error(`MACHINE_TRANSPORT=${config.transport} has no MQTT endpoint (MACHINE_MQTT_URL)`);
24
+ return config.mqtt;
25
+ }
26
+ /** The codec for a line when the host supplies none: the line protocol under MACHINE_TOPIC_ROOT. */
27
+ function defaultCodec(config, line) {
28
+ return (0, protocol_1.genericCodec)({ root: config.topicRoot, line });
29
+ }
30
+ /** The gateway's view of every machine on the line. */
31
+ function createMachineBus(config, line, codec = defaultCodec(config, line)) {
32
+ switch (config.transport) {
33
+ case config_1.MACHINE_TRANSPORT.NATS:
34
+ return new nats_bus_1.NatsMachineBus({
35
+ natsUrl: config.siteNatsUrl, user: config.siteNatsUser, password: config.siteNatsPassword,
36
+ mqtt: mqttOf(config), controllerId: (0, line_1.controllerId)(line), siteId: line.siteId, lineId: line.lineId, codec,
37
+ });
38
+ case config_1.MACHINE_TRANSPORT.MQTT:
39
+ return new mqtt_bus_1.MqttMachineBus(mqttOf(config), (0, line_1.controllerId)(line), (0, protocol_1.lineFilter)(codec.root, line.siteId, line.lineId));
40
+ default:
41
+ throw new Error(`MACHINE_TRANSPORT=${config.transport} ${IN_PROCESS_ONLY}`);
42
+ }
43
+ }
44
+ /** Long Tail's escalation events for the gateway; `in-process` needs the running instance's callback adapter. */
45
+ function createEscalationFeed(config, line, callbacks) {
46
+ switch (config.feed) {
47
+ case config_1.ESCALATION_FEED.IN_PROCESS:
48
+ if (!callbacks)
49
+ throw new Error('MACHINE_ESCALATION_FEED=in-process runs inside start({ machines })');
50
+ return new callback_feed_1.CallbackEscalationFeed(callbacks);
51
+ case config_1.ESCALATION_FEED.CORE:
52
+ return new nats_feed_1.CoreEscalationFeed({ natsUrl: config.ltNatsUrl, token: config.ltNatsToken });
53
+ default:
54
+ throw new Error(`MACHINE_ESCALATION_FEED=${config.feed} ${IN_PROCESS_ONLY}`);
55
+ }
56
+ }
57
+ /** One MQTT link per simulated machine; machines speak MQTT in every networked mode. */
58
+ function createLinkFactory(config) {
59
+ if (config.transport === config_1.MACHINE_TRANSPORT.MEMORY)
60
+ throw new Error(`MACHINE_TRANSPORT=memory ${IN_PROCESS_ONLY}`);
61
+ const endpoint = mqttOf(config);
62
+ return () => new mqtt_link_1.MqttMachineLink(endpoint);
63
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The machine surface inside Long Tail. For each line this deployment
3
+ * controls, one replica leads (an advisory lock), makes sure the line's bot
4
+ * accounts exist, and runs the gateway: escalation events in, machine
5
+ * messages in, verbs performed in process as each machine. Workflows never
6
+ * see any of it; their rows are resolved the way a form resolves them.
7
+ */
8
+ import type { CallbackEventAdapter } from '../../lib/events/callback';
9
+ import type { LineDef } from '../../lib/machines/line';
10
+ import { type MachineLineConfig } from './config';
11
+ import { type GatewayLog } from './gateway/log';
12
+ import type { LineCodec } from '../../lib/machines/protocol';
13
+ import type { HandoffRowAdapter } from './rows';
14
+ type Env = Record<string, string | undefined>;
15
+ export interface MachineSurfaceOptions {
16
+ lines: LineDef[];
17
+ /** Transport settings (MACHINE_*); defaults to process.env. */
18
+ env?: Env;
19
+ callbacks?: CallbackEventAdapter;
20
+ log?: GatewayLog;
21
+ codec?: LineCodec;
22
+ rows?: HandoffRowAdapter;
23
+ }
24
+ /** Inside Long Tail the gateway reads escalation events in process. */
25
+ export declare function surfaceConfig(env: Env): MachineLineConfig;
26
+ export declare class MachineSurface {
27
+ private readonly runs;
28
+ private constructor();
29
+ /** Validate the configuration, then contend for every line in the background. */
30
+ static start(opts: MachineSurfaceOptions): MachineSurface;
31
+ stop(): Promise<void>;
32
+ }
33
+ export {};
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * The machine surface inside Long Tail. For each line this deployment
4
+ * controls, one replica leads (an advisory lock), makes sure the line's bot
5
+ * accounts exist, and runs the gateway: escalation events in, machine
6
+ * messages in, verbs performed in process as each machine. Workflows never
7
+ * see any of it; their rows are resolved the way a form resolves them.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.MachineSurface = void 0;
11
+ exports.surfaceConfig = surfaceConfig;
12
+ const config_1 = require("./config");
13
+ const api_escalations_1 = require("./gateway/api-escalations");
14
+ const gateway_1 = require("./gateway/gateway");
15
+ const log_1 = require("./gateway/log");
16
+ const identities_1 = require("./identities");
17
+ const leadership_1 = require("./leadership");
18
+ const select_1 = require("./select");
19
+ /** Inside Long Tail the gateway reads escalation events in process. */
20
+ function surfaceConfig(env) {
21
+ const transport = env.MACHINE_TRANSPORT;
22
+ if (transport !== config_1.MACHINE_TRANSPORT.NATS && transport !== config_1.MACHINE_TRANSPORT.MQTT) {
23
+ throw new Error(`start({ machines }) needs MACHINE_TRANSPORT=nats or mqtt (got "${transport ?? ''}")`);
24
+ }
25
+ return (0, config_1.loadMachineLineConfig)({ ...env, MACHINE_ESCALATION_FEED: env.MACHINE_ESCALATION_FEED ?? config_1.ESCALATION_FEED.IN_PROCESS });
26
+ }
27
+ class LineRun {
28
+ constructor(line, config, opts, log) {
29
+ this.line = line;
30
+ this.config = config;
31
+ this.opts = opts;
32
+ this.log = log;
33
+ this.leadership = new leadership_1.LineLeadership(`lt-machine-line:${line.siteId}/${line.lineId}`);
34
+ }
35
+ start() {
36
+ const where = { site: this.line.siteId, line: this.line.lineId };
37
+ this.log.info(log_1.GATEWAY_LOG.STANDBY, where);
38
+ this.leadership.run(() => this.lead(), async (err) => {
39
+ this.log.warn(log_1.GATEWAY_LOG.LEADERSHIP_LOST, { ...where, error: err.message });
40
+ await this.gateway?.stop();
41
+ this.gateway = undefined;
42
+ }).catch((err) => this.log.error(log_1.GATEWAY_LOG.START_FAILED, { ...where, error: err.message }));
43
+ }
44
+ async stop() {
45
+ await this.gateway?.stop();
46
+ await this.leadership.stop();
47
+ }
48
+ async lead() {
49
+ // 1. Identities, 2. gateway on the line's bus and feed, 3. reconcile and consume (gateway.start)
50
+ const ids = await (0, identities_1.ensureLineBots)(this.line);
51
+ const userFor = (as) => {
52
+ const id = ids.get(as);
53
+ if (!id)
54
+ throw new Error(`no bot for "${as}" on ${this.line.siteId}/${this.line.lineId}`);
55
+ return id;
56
+ };
57
+ const { config, line } = this;
58
+ const codec = this.opts.codec ?? (0, select_1.defaultCodec)(config, line);
59
+ this.gateway = new gateway_1.MachineGateway({
60
+ bus: (0, select_1.createMachineBus)(config, line, codec),
61
+ codec,
62
+ rows: this.opts.rows,
63
+ feed: (0, select_1.createEscalationFeed)(config, line, this.opts.callbacks),
64
+ lt: new api_escalations_1.ApiLineEscalations(userFor),
65
+ addresses: new identities_1.BotAddressBook(ids),
66
+ line,
67
+ log: this.log,
68
+ settings: {
69
+ topicRoot: config.topicRoot, replyTimeoutMs: config.replyTimeoutMs, dispatchAttempts: config.dispatchAttempts,
70
+ offlineDebounceMs: config.offlineDebounceMs, dispatchTtlMs: config.dispatchTtlMs, quietMs: config.quietMs,
71
+ receiptDelayMs: config.receiptDelayMs,
72
+ },
73
+ });
74
+ await this.gateway.start();
75
+ this.log.info(log_1.GATEWAY_LOG.LEADING, { site: line.siteId, line: line.lineId });
76
+ }
77
+ }
78
+ class MachineSurface {
79
+ constructor(runs) {
80
+ this.runs = runs;
81
+ }
82
+ /** Validate the configuration, then contend for every line in the background. */
83
+ static start(opts) {
84
+ if (!opts.lines.length)
85
+ throw new Error('start({ machines }) needs at least one line');
86
+ const config = surfaceConfig(opts.env ?? process.env);
87
+ const log = opts.log ?? log_1.consoleLog;
88
+ const runs = opts.lines.map((line) => new LineRun(line, config, opts, log));
89
+ for (const run of runs)
90
+ run.start();
91
+ return new MachineSurface(runs);
92
+ }
93
+ async stop() {
94
+ for (const run of this.runs)
95
+ await run.stop();
96
+ }
97
+ }
98
+ exports.MachineSurface = MachineSurface;