@hotmeshio/long-tail 0.25.4 → 0.27.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 (173) hide show
  1. package/README.md +1 -0
  2. package/build/index.d.ts +5 -0
  3. package/build/index.js +36 -7
  4. package/build/lib/events/nats.d.ts +11 -0
  5. package/build/lib/events/nats.js +33 -2
  6. package/build/lib/machines/index.d.ts +6 -0
  7. package/build/lib/machines/index.js +22 -0
  8. package/build/lib/machines/line.d.ts +44 -0
  9. package/build/lib/machines/line.js +114 -0
  10. package/build/lib/machines/protocol/codec.d.ts +108 -0
  11. package/build/lib/machines/protocol/codec.js +80 -0
  12. package/build/lib/machines/protocol/constants.d.ts +120 -0
  13. package/build/lib/machines/protocol/constants.js +97 -0
  14. package/build/lib/machines/protocol/contract.d.ts +19 -0
  15. package/build/lib/machines/protocol/contract.js +141 -0
  16. package/build/lib/machines/protocol/envelope.d.ts +14 -0
  17. package/build/lib/machines/protocol/envelope.js +43 -0
  18. package/build/lib/machines/protocol/index.d.ts +5 -0
  19. package/build/lib/machines/protocol/index.js +21 -0
  20. package/build/lib/machines/protocol/topics.d.ts +33 -0
  21. package/build/lib/machines/protocol/topics.js +79 -0
  22. package/build/lib/machines/site/edge-account.d.ts +11 -0
  23. package/build/lib/machines/site/edge-account.js +24 -0
  24. package/build/lib/machines/site/mosquitto-conf.d.ts +27 -0
  25. package/build/lib/machines/site/mosquitto-conf.js +81 -0
  26. package/build/lib/machines/site/site-conf.d.ts +23 -0
  27. package/build/lib/machines/site/site-conf.js +62 -0
  28. package/build/lib/machines/transport/callback-feed.d.ts +18 -0
  29. package/build/lib/machines/transport/callback-feed.js +31 -0
  30. package/build/lib/machines/transport/delivery.d.ts +20 -0
  31. package/build/lib/machines/transport/delivery.js +37 -0
  32. package/build/lib/machines/transport/index.d.ts +13 -0
  33. package/build/lib/machines/transport/index.js +29 -0
  34. package/build/lib/machines/transport/memory-broker.d.ts +35 -0
  35. package/build/lib/machines/transport/memory-broker.js +145 -0
  36. package/build/lib/machines/transport/memory.d.ts +40 -0
  37. package/build/lib/machines/transport/memory.js +79 -0
  38. package/build/lib/machines/transport/mqtt-bus.d.ts +24 -0
  39. package/build/lib/machines/transport/mqtt-bus.js +43 -0
  40. package/build/lib/machines/transport/mqtt-link.d.ts +16 -0
  41. package/build/lib/machines/transport/mqtt-link.js +37 -0
  42. package/build/lib/machines/transport/mqtt-session.d.ts +34 -0
  43. package/build/lib/machines/transport/mqtt-session.js +127 -0
  44. package/build/lib/machines/transport/nats-bus.d.ts +36 -0
  45. package/build/lib/machines/transport/nats-bus.js +47 -0
  46. package/build/lib/machines/transport/nats-feed.d.ts +26 -0
  47. package/build/lib/machines/transport/nats-feed.js +49 -0
  48. package/build/lib/machines/transport/pubsub-feed.d.ts +17 -0
  49. package/build/lib/machines/transport/pubsub-feed.js +39 -0
  50. package/build/lib/machines/transport/serial.d.ts +21 -0
  51. package/build/lib/machines/transport/serial.js +54 -0
  52. package/build/lib/machines/transport/stream-bus.d.ts +40 -0
  53. package/build/lib/machines/transport/stream-bus.js +57 -0
  54. package/build/lib/machines/transport/streams.d.ts +24 -0
  55. package/build/lib/machines/transport/streams.js +49 -0
  56. package/build/lib/messaging/config.d.ts +25 -0
  57. package/build/lib/messaging/config.js +55 -0
  58. package/build/lib/messaging/constants.d.ts +26 -0
  59. package/build/lib/messaging/constants.js +26 -0
  60. package/build/lib/messaging/index.d.ts +24 -0
  61. package/build/lib/messaging/index.js +65 -0
  62. package/build/lib/messaging/memory.d.ts +16 -0
  63. package/build/lib/messaging/memory.js +147 -0
  64. package/build/lib/messaging/nats/permissions.d.ts +13 -0
  65. package/build/lib/messaging/nats/permissions.js +22 -0
  66. package/build/lib/messaging/nats/provider.d.ts +30 -0
  67. package/build/lib/messaging/nats/provider.js +77 -0
  68. package/build/lib/messaging/nats/render-conf.d.ts +68 -0
  69. package/build/lib/messaging/nats/render-conf.js +58 -0
  70. package/build/lib/messaging/nats/streams.d.ts +26 -0
  71. package/build/lib/messaging/nats/streams.js +97 -0
  72. package/build/lib/messaging/subjects.d.ts +8 -0
  73. package/build/lib/messaging/subjects.js +25 -0
  74. package/build/scripts/bambu-boat.d.ts +17 -0
  75. package/build/scripts/bambu-boat.js +134 -0
  76. package/build/scripts/bambu-print.d.ts +18 -0
  77. package/build/scripts/bambu-print.js +114 -0
  78. package/build/scripts/machine-line.d.ts +33 -0
  79. package/build/scripts/machine-line.js +301 -0
  80. package/build/services/machines/commands.d.ts +29 -0
  81. package/build/services/machines/commands.js +37 -0
  82. package/build/services/machines/config.d.ts +49 -0
  83. package/build/services/machines/config.js +94 -0
  84. package/build/services/machines/contracts.d.ts +210 -0
  85. package/build/services/machines/contracts.js +124 -0
  86. package/build/services/machines/forms.d.ts +55 -0
  87. package/build/services/machines/forms.js +37 -0
  88. package/build/services/machines/gateway/actions.d.ts +111 -0
  89. package/build/services/machines/gateway/actions.js +8 -0
  90. package/build/services/machines/gateway/address-book.d.ts +16 -0
  91. package/build/services/machines/gateway/address-book.js +24 -0
  92. package/build/services/machines/gateway/admission.d.ts +35 -0
  93. package/build/services/machines/gateway/admission.js +66 -0
  94. package/build/services/machines/gateway/api-escalations.d.ts +23 -0
  95. package/build/services/machines/gateway/api-escalations.js +101 -0
  96. package/build/services/machines/gateway/dispatcher.d.ts +76 -0
  97. package/build/services/machines/gateway/dispatcher.js +197 -0
  98. package/build/services/machines/gateway/escalations.d.ts +51 -0
  99. package/build/services/machines/gateway/escalations.js +40 -0
  100. package/build/services/machines/gateway/executor.d.ts +60 -0
  101. package/build/services/machines/gateway/executor.js +221 -0
  102. package/build/services/machines/gateway/gateway.d.ts +88 -0
  103. package/build/services/machines/gateway/gateway.js +270 -0
  104. package/build/services/machines/gateway/index.d.ts +10 -0
  105. package/build/services/machines/gateway/index.js +26 -0
  106. package/build/services/machines/gateway/jobs.d.ts +16 -0
  107. package/build/services/machines/gateway/jobs.js +24 -0
  108. package/build/services/machines/gateway/lifecycle.d.ts +21 -0
  109. package/build/services/machines/gateway/lifecycle.js +89 -0
  110. package/build/services/machines/gateway/log.d.ts +41 -0
  111. package/build/services/machines/gateway/log.js +42 -0
  112. package/build/services/machines/gateway/lt-http.d.ts +51 -0
  113. package/build/services/machines/gateway/lt-http.js +120 -0
  114. package/build/services/machines/gateway/pacing.d.ts +43 -0
  115. package/build/services/machines/gateway/pacing.js +158 -0
  116. package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
  117. package/build/services/machines/gateway/plan-escalation.js +210 -0
  118. package/build/services/machines/gateway/plan-machine.d.ts +17 -0
  119. package/build/services/machines/gateway/plan-machine.js +283 -0
  120. package/build/services/machines/gateway/plan-shared.d.ts +18 -0
  121. package/build/services/machines/gateway/plan-shared.js +36 -0
  122. package/build/services/machines/gateway/provision.d.ts +22 -0
  123. package/build/services/machines/gateway/provision.js +79 -0
  124. package/build/services/machines/gateway/receipts.d.ts +33 -0
  125. package/build/services/machines/gateway/receipts.js +65 -0
  126. package/build/services/machines/gateway/reconcile.d.ts +35 -0
  127. package/build/services/machines/gateway/reconcile.js +99 -0
  128. package/build/services/machines/gateway/registration.d.ts +38 -0
  129. package/build/services/machines/gateway/registration.js +121 -0
  130. package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
  131. package/build/services/machines/gateway/reply-waiter.js +65 -0
  132. package/build/services/machines/gateway/state.d.ts +145 -0
  133. package/build/services/machines/gateway/state.js +241 -0
  134. package/build/services/machines/gateway/trace.d.ts +16 -0
  135. package/build/services/machines/gateway/trace.js +43 -0
  136. package/build/services/machines/identities.d.ts +25 -0
  137. package/build/services/machines/identities.js +77 -0
  138. package/build/services/machines/index.d.ts +11 -0
  139. package/build/services/machines/index.js +27 -0
  140. package/build/services/machines/leadership.d.ts +35 -0
  141. package/build/services/machines/leadership.js +100 -0
  142. package/build/services/machines/roles.d.ts +13 -0
  143. package/build/services/machines/roles.js +62 -0
  144. package/build/services/machines/rows.d.ts +30 -0
  145. package/build/services/machines/rows.js +71 -0
  146. package/build/services/machines/select.d.ts +25 -0
  147. package/build/services/machines/select.js +76 -0
  148. package/build/services/machines/surface.d.ts +33 -0
  149. package/build/services/machines/surface.js +104 -0
  150. package/build/services/machines/testing/fake-long-tail.d.ts +79 -0
  151. package/build/services/machines/testing/fake-long-tail.js +158 -0
  152. package/build/services/machines/testing/index.d.ts +3 -0
  153. package/build/services/machines/testing/index.js +19 -0
  154. package/build/services/machines/testing/recording-log.d.ts +14 -0
  155. package/build/services/machines/testing/recording-log.js +22 -0
  156. package/build/services/machines/testing/wait.d.ts +3 -0
  157. package/build/services/machines/testing/wait.js +17 -0
  158. package/build/services/mcp/db-server/schemas.d.ts +2 -2
  159. package/build/start/adapters.js +11 -2
  160. package/build/start/index.js +20 -1
  161. package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
  162. package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
  163. package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
  164. package/build/tsconfig.tsbuildinfo +1 -1
  165. package/build/types/index.d.ts +3 -1
  166. package/build/types/machines.d.ts +167 -0
  167. package/build/types/machines.js +7 -0
  168. package/build/types/messaging.d.ts +72 -0
  169. package/build/types/messaging.js +11 -0
  170. package/build/types/startup.d.ts +49 -1
  171. package/docs/events.md +50 -4
  172. package/docs/machines.md +322 -0
  173. package/package.json +13 -1
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ /**
3
+ * machine-line: drive the machine line example end to end.
4
+ *
5
+ * Starts unit routes on the factory line and plays the people: at every
6
+ * station people work (and at the inspector, where every route ends) a
7
+ * person takes the units off the station before and receives them; a
8
+ * technician clears alarms; an operator decides each failed handoff. The
9
+ * machines and the gateway do the rest over MQTT. MIX sets the share of
10
+ * routes minted from the automated manifest; the rest run the manual one,
11
+ * through the same workflow. Rows are watched on Long Tail's NATS.
12
+ *
13
+ * Usage (after `docker compose -f docker-compose.yml -f docker-compose.nats.yml -f docker-compose.machines.yml up -d --build`):
14
+ * npm run machine:line
15
+ * MIX=50 UNITS=8 npm run machine:line # half automated, half manual, same factory
16
+ * UNITS=8 BATCH=2 npm run machine:line
17
+ * FAULT=jam npm run machine:line # first route jams leaving FAULT_AT; operator retries
18
+ * FAULT=reject DECIDE=scrap npm run machine:line
19
+ *
20
+ * Env vars:
21
+ * BASE_URL Long Tail API (default http://localhost:3000)
22
+ * SIM_URL mock line drill server (default http://localhost:8090)
23
+ * NATS_URL Long Tail NATS (default nats://localhost:4222), NATS_TOKEN (default dev_api_secret)
24
+ * UNITS units to run (default 6); BATCH units per route (default 2)
25
+ * MIX percent of routes on the automated manifest (default 100); the rest run manual
26
+ * MACHINE_LINE_CONFIG, MACHINE_MANIFESTS line and manifests (default the factory line and route-manifests.json)
27
+ * FAULT jam | incomplete | carrier_mismatch | unreadable | stall | reject | hold | estop | offline
28
+ * FAULT_AT machine the fault happens at (default the first machine)
29
+ * DECIDE operator decision on a failed handoff: retry | manual | scrap (default retry;
30
+ * manual where the row does not offer retry)
31
+ * OFFLINE_S how long FAULT=offline keeps the machine away (default 25)
32
+ * HANDOFF_TIMEOUT per-handoff timeout (default 5m); TIMEOUT_S whole run (default 300)
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ try {
36
+ require('dotenv/config');
37
+ }
38
+ catch { /* dotenv optional */ }
39
+ const nats_1 = require("nats");
40
+ const contracts_1 = require("../services/machines/contracts");
41
+ const line_1 = require("../lib/machines/line");
42
+ const manifests_1 = require("../examples/workflows/machine-line/manifests");
43
+ const line_2 = require("../examples/workflows/machine-line/line");
44
+ const protocol_1 = require("../lib/machines/protocol");
45
+ const route_1 = require("../examples/workflows/machine-line/workflows/route");
46
+ const env = process.env;
47
+ const BASE_URL = env.BASE_URL || 'http://localhost:3000';
48
+ const SIM_URL = env.SIM_URL || 'http://localhost:8090';
49
+ const NATS_URL = env.NATS_URL || 'nats://localhost:4222';
50
+ const NATS_TOKEN = env.NATS_TOKEN ?? 'dev_api_secret';
51
+ const UNITS = parseInt(env.UNITS || '6', 10);
52
+ const BATCH = Math.max(1, parseInt(env.BATCH || '2', 10));
53
+ const MIX = parseInt(env.MIX || '100', 10);
54
+ const FAULT = env.FAULT || '';
55
+ const DECIDE = env.DECIDE || contracts_1.DECISION.RETRY;
56
+ const OFFLINE_S = parseInt(env.OFFLINE_S || '25', 10);
57
+ const HANDOFF_TIMEOUT = env.HANDOFF_TIMEOUT || '5m';
58
+ const TIMEOUT_S = parseInt(env.TIMEOUT_S || '300', 10);
59
+ const TECH = 'line-tech';
60
+ const TRANSFER_FAULTS = ['jam', 'incomplete', 'carrier_mismatch', 'unreadable', 'stall'];
61
+ const RESULT_FAULTS = ['reject', 'hold'];
62
+ const STOPPED = new Set([protocol_1.MODE.FAULTED, protocol_1.MODE.EMERGENCY_STOP]);
63
+ const line = (0, line_2.loadLine)(env.MACHINE_LINE_CONFIG || manifests_1.FACTORY_LINE_PATH);
64
+ const manifests = (0, manifests_1.loadManifests)(env.MACHINE_MANIFESTS || undefined);
65
+ const MANIFEST = { AUTOMATED: 'automated', MANUAL: 'manual' };
66
+ const machineRoles = new Set((0, line_1.stationRoles)(line));
67
+ const firstMachine = manifests[MANIFEST.AUTOMATED].steps.find((s) => typeof s === 'string');
68
+ const FAULT_AT = env.FAULT_AT || firstMachine;
69
+ const manifestOf = new Map();
70
+ const RUN = `L${Date.now().toString(36).slice(-5)}`;
71
+ const counts = {};
72
+ let token = '';
73
+ const ts = () => new Date().toISOString().slice(11, 19);
74
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
75
+ const say = (msg) => console.log(`[${ts()}] ${msg}`);
76
+ async function call(base, method, path, body, auth = true) {
77
+ const res = await fetch(`${base}${path}`, {
78
+ method,
79
+ headers: { 'content-type': 'application/json', ...(auth && token ? { authorization: `Bearer ${token}` } : {}) },
80
+ body: body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body),
81
+ });
82
+ const data = await res.json().catch(() => ({}));
83
+ if (!res.ok)
84
+ throw new Error(`${method} ${path} -> ${res.status}: ${JSON.stringify(data)}`);
85
+ return data;
86
+ }
87
+ const api = (method, path, body) => call(BASE_URL, method, `/api${path}`, body);
88
+ const sim = (method, path, body) => call(SIM_URL, method, path, body, false);
89
+ // ── The physical world ───────────────────────────────────────────────────────
90
+ /** A technician clears every alarm at a machine and resets it if it stopped. */
91
+ async function fixMachine(machineId) {
92
+ const state = await sim('GET', `/sim/${machineId}`);
93
+ for (const alarm of state.alarms ?? []) {
94
+ if (alarm.active)
95
+ await sim('POST', `/sim/${machineId}/clear-alarm`, { alarmId: alarm.alarmId, by: TECH });
96
+ }
97
+ if (STOPPED.has(state.mode))
98
+ await sim('POST', `/sim/${machineId}/reset`, { by: TECH });
99
+ if (!state.link?.connected)
100
+ await sim('POST', `/sim/${machineId}/network`, { mode: 'restore' });
101
+ }
102
+ async function injectFault() {
103
+ if (!FAULT)
104
+ return;
105
+ if (TRANSFER_FAULTS.includes(FAULT))
106
+ await sim('POST', `/sim/${FAULT_AT}/transfer`, { fault: FAULT });
107
+ else if (RESULT_FAULTS.includes(FAULT))
108
+ await sim('POST', `/sim/${FAULT_AT}/arm`, { result: FAULT, reason: `driver-${FAULT}` });
109
+ else if (FAULT === 'estop')
110
+ await sim('POST', `/sim/${FAULT_AT}/emergency-stop`);
111
+ else if (FAULT === 'offline') {
112
+ await sim('POST', `/sim/${FAULT_AT}/network`, { mode: 'drop' });
113
+ setTimeout(() => void sim('POST', `/sim/${FAULT_AT}/network`, { mode: 'restore' }).then(() => say(`[floor] ${FAULT_AT} back online`)), OFFLINE_S * 1000);
114
+ }
115
+ else
116
+ throw new Error(`unknown FAULT=${FAULT}`);
117
+ say(`[floor] fault armed: ${FAULT} at ${FAULT_AT}`);
118
+ }
119
+ // ── The people ───────────────────────────────────────────────────────────────
120
+ /** A person takes units off a machine; false when a unit was not there to take. */
121
+ async function takeUnits(machineId, units, tries = 40) {
122
+ if (!machineId)
123
+ return true;
124
+ let all = true;
125
+ for (const unit of units) {
126
+ let taken = false;
127
+ for (let i = 0; i < tries && !(taken = (await sim('POST', `/sim/${machineId}/unload`, { unitId: unit.unitId })).result); i++)
128
+ await sleep(250);
129
+ all &&= taken;
130
+ }
131
+ return all;
132
+ }
133
+ /** Wait until the row holds the sender's release of every unit: the machine has finished them. */
134
+ async function released(rowId, units, ms = 180_000) {
135
+ const deadline = Date.now() + ms;
136
+ while (Date.now() < deadline) {
137
+ const { items = [] } = await api('GET', `/escalations/${rowId}/items`);
138
+ const keys = new Set(items.map((i) => i.itemKey));
139
+ if (units.every((u) => keys.has(`released:${u.unitId}`)))
140
+ return true;
141
+ await sleep(500);
142
+ }
143
+ return false;
144
+ }
145
+ /** Units the operator sent off the line by hand: the sender already finished them, so no release is coming. */
146
+ const takenOver = new Set();
147
+ /**
148
+ * A person at a station: take each unit off the machine it came from (if any),
149
+ * or collect it where a split left it (a pickup row), then receive the set.
150
+ */
151
+ async function receive(row) {
152
+ const md = row.metadata ?? {};
153
+ const units = md.units ?? [];
154
+ const at = md.pickupAt ?? md.fromMachine ?? null;
155
+ const finished = async () => !!md.pickupAt || units.every((u) => takenOver.has(u.unitId)) || released(row.id, units);
156
+ if (at && !(await finished() && await takeUnits(at, units))) {
157
+ say(`[${row.role}]`.padEnd(11) + ` ${units.map((u) => u.unitId).join(', ')} never left ${at}; leaving the row open`);
158
+ return;
159
+ }
160
+ await api('POST', `/escalations/${row.id}/resolve`, { resolverPayload: { received: true, receiver: `${row.role}-person` } });
161
+ say(`[${row.role}]`.padEnd(11) + ` received ${units.map((u) => u.unitId).join(', ')} ${md.pickupAt ? `collected at ${at}` : `from ${at ?? 'a person'}`}`);
162
+ }
163
+ /** Set once a route left the line on purpose (manual or scrap), so an incomplete run is expected. */
164
+ let offLine = false;
165
+ /** Operator: make the machines healthy, then decide the failed handoff; retry falls back to manual where the row does not offer it. */
166
+ async function decideFailure(row) {
167
+ const md = row.metadata ?? {};
168
+ for (const m of line.machines)
169
+ await fixMachine(m.machineId);
170
+ const allowed = md.decisions ?? [DECIDE];
171
+ const decision = allowed.includes(DECIDE) ? DECIDE : allowed[0];
172
+ if (decision !== contracts_1.DECISION.RETRY)
173
+ offLine = true;
174
+ if (decision === contracts_1.DECISION.MANUAL)
175
+ for (const u of md.units ?? [])
176
+ takenOver.add(u.unitId);
177
+ if (decision === contracts_1.DECISION.SCRAP)
178
+ await takeUnits(md.fromMachine ?? null, md.units ?? [], 4);
179
+ await api('POST', `/escalations/${row.id}/resolve`, { resolverPayload: { decision, note: `driver: ${md.errorCode}` } });
180
+ // Scrapped units split by a partial arrival: the ones that arrived wait at the receiver once the workflow stops its job.
181
+ if (decision === contracts_1.DECISION.SCRAP && md.errorCode === 'partial-arrival')
182
+ await takeUnits(md.toMachine ?? null, md.units ?? [], 8);
183
+ say(`[operator] ${md.jobId} ${md.errorCode} at ${md.fromMachine ?? 'entry'} -> ${decision}`);
184
+ }
185
+ async function onRow(row) {
186
+ const md = row.metadata ?? {};
187
+ if (md.siteId !== line.siteId || md.lineId !== line.lineId)
188
+ return;
189
+ counts[row.type] = (counts[row.type] ?? 0) + 1;
190
+ if (row.type === contracts_1.ROW_TYPE.HANDOFF && !machineRoles.has(row.role) && String(md.jobId ?? '').startsWith(RUN)) {
191
+ // Each person works their own station; a slow one does not hold up the rest.
192
+ void receive(row).catch((err) => say(`[driver] ${err.message}`));
193
+ return;
194
+ }
195
+ if (row.type === contracts_1.ROW_TYPE.HANDOFF)
196
+ return;
197
+ if (row.type === contracts_1.ROW_TYPE.HANDOFF_ERROR && String(md.jobId ?? '').startsWith(RUN))
198
+ return decideFailure(row);
199
+ if (row.type === contracts_1.ROW_TYPE.ALARM) {
200
+ say(`[tech] alarm ${md.code ?? ''} at ${md.machineId}: fixing`);
201
+ await sleep(1500);
202
+ if (md.machineId)
203
+ await fixMachine(md.machineId);
204
+ return;
205
+ }
206
+ say(`[row] ${row.type} ${row.description ?? ''}`);
207
+ }
208
+ // ── The run ──────────────────────────────────────────────────────────────────
209
+ async function startRoutes(artifact) {
210
+ const ids = [];
211
+ for (let i = 0, n = 0; n < UNITS; i++) {
212
+ const routeKey = `${RUN}-R${i + 1}`;
213
+ const units = Array.from({ length: Math.min(BATCH, UNITS - n) }, (_, j) => ({ unitId: `${routeKey}-U${j + 1}`, carrierId: `CR-${routeKey}-${j + 1}` }));
214
+ n += units.length;
215
+ const name = (0, manifests_1.automatedAt)(i, MIX) ? MANIFEST.AUTOMATED : MANIFEST.MANUAL;
216
+ const data = {
217
+ siteId: line.siteId, lineId: line.lineId, routeKey, orderRef: `ORD-${RUN}`, units,
218
+ steps: (0, manifests_1.manifestSteps)(line, manifests[name]), exitRole: (0, line_1.exitRole)(line), artifact, handoffTimeout: HANDOFF_TIMEOUT,
219
+ };
220
+ const workflowId = `mur-${routeKey}`;
221
+ await api('POST', `/workflows/${route_1.UNIT_ROUTE_WORKFLOW}/invoke`, { workflowId, data, metadata: { source: 'machine-line', manifest: name } });
222
+ ids.push(workflowId);
223
+ manifestOf.set(workflowId, name);
224
+ say(`[route] ${routeKey} (${name}): ${units.map((u) => u.unitId).join(', ')}`);
225
+ }
226
+ return ids;
227
+ }
228
+ async function awaitResults(ids) {
229
+ const results = new Map();
230
+ const deadline = Date.now() + TIMEOUT_S * 1000;
231
+ while (results.size < ids.length && Date.now() < deadline) {
232
+ await sleep(1000);
233
+ for (const id of ids.filter((x) => !results.has(x))) {
234
+ const resp = await api('GET', `/workflows/${id}/result`).catch(() => null);
235
+ if (resp?.result?.data?.status)
236
+ results.set(id, resp.result.data);
237
+ }
238
+ }
239
+ return results;
240
+ }
241
+ function report(ids, results) {
242
+ console.log('\nroute manifest status hops');
243
+ let ok = true;
244
+ const byManifest = {};
245
+ for (const id of ids) {
246
+ const data = results.get(id);
247
+ const name = manifestOf.get(id);
248
+ const tally = (byManifest[name] ??= { routes: 0, complete: 0, atInspector: 0 });
249
+ tally.routes++;
250
+ if (data?.status === 'complete')
251
+ tally.complete++;
252
+ else
253
+ ok = false;
254
+ const last = data?.history?.[data.history.length - 1];
255
+ if (last?.to === 'exit' && last.received)
256
+ tally.atInspector++;
257
+ const hops = (data?.history ?? []).map((h) => `${h.to}${h.received ? '' : `(${h.errorCode})`}`).join(' > ');
258
+ console.log(`${id.padEnd(24)} ${name.padEnd(10)} ${(data?.status ?? 'timed out').padEnd(10)} ${hops}`);
259
+ }
260
+ console.log('');
261
+ for (const [name, t] of Object.entries(byManifest)) {
262
+ console.log(`${name.padEnd(10)} ${t.complete}/${t.routes} complete, ${t.atInspector} received at ${(0, line_1.exitRole)(line)}`);
263
+ }
264
+ console.log(`\nrows seen: ${Object.entries(counts).map(([k, v]) => `${k}=${v}`).join(' ') || 'none'}`);
265
+ return ok;
266
+ }
267
+ async function main() {
268
+ // 1. Log in, check the mock line is this line
269
+ token = (await call(BASE_URL, 'POST', '/api/auth/login', { username: 'superadmin', password: 'l0ngt@1l' }, false)).token;
270
+ const state = await sim('GET', '/sim/state');
271
+ if (state.siteId !== line.siteId || state.lineId !== line.lineId)
272
+ throw new Error(`mock runs ${state.siteId}/${state.lineId}, driver expects ${line.siteId}/${line.lineId}`);
273
+ for (const m of line.machines) {
274
+ await sim('POST', `/sim/${m.machineId}/disarm`);
275
+ await fixMachine(m.machineId);
276
+ }
277
+ // 2. Watch the rows people act on, one at a time
278
+ const nc = await (0, nats_1.connect)({ servers: NATS_URL, token: NATS_TOKEN || undefined, name: 'machine-line-driver' });
279
+ const sc = (0, nats_1.StringCodec)();
280
+ let chain = Promise.resolve();
281
+ nc.subscribe('lt.events.system.escalation.*.*.created', {
282
+ callback: (_err, m) => {
283
+ const row = JSON.parse(sc.decode(m.data)).data ?? {};
284
+ chain = chain.then(() => onRow(row)).catch((err) => say(`[driver] ${err.message}`));
285
+ },
286
+ });
287
+ // 3. A job artifact, the fault, and the routes
288
+ const artifact = await sim('POST', '/payloads', `machine-line ${RUN}`);
289
+ await injectFault();
290
+ const ids = await startRoutes({ url: artifact.url, sha256: artifact.sha256 });
291
+ // 4. Wait for every route, then report
292
+ const results = await awaitResults(ids);
293
+ await chain;
294
+ await nc.drain();
295
+ const ok = report(ids, results);
296
+ process.exit(ok || offLine ? 0 : 1);
297
+ }
298
+ main().catch((err) => {
299
+ console.error(`[${ts()}] failed: ${err.message}`);
300
+ process.exit(1);
301
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Commands as rows. A host asks a machine to start, stop, pause, resume,
3
+ * change over or cancel a job by parking a machine-command row on the machine's role, from a
4
+ * workflow step or through the API, the same way it asks a person for
5
+ * anything. The gateway sends the command and resolves the row with the
6
+ * machine's reply (CommandResolution), so the step reads the outcome as data.
7
+ */
8
+ import type { Op } from '../../lib/machines/protocol';
9
+ import { type LineDef } from '../../lib/machines/line';
10
+ import { type CommandFacets } from './contracts';
11
+ /** Where a command goes: the machine and the role its rows land on. */
12
+ export interface CommandTarget {
13
+ siteId: string;
14
+ lineId: string;
15
+ machineId: string;
16
+ role: string;
17
+ }
18
+ export declare function commandTarget(line: LineDef, machineId: string): CommandTarget;
19
+ /** The row that asks a machine for `op`: pass it to conditional(), or create it through the API. */
20
+ export declare function commandRow(target: CommandTarget, op: Op, args?: Record<string, unknown>): {
21
+ role: string;
22
+ type: "machine-command";
23
+ subtype: Op;
24
+ description: string;
25
+ priority: number;
26
+ metadata: Record<string, unknown>;
27
+ };
28
+ /** A machine-command row's facets; null when the line, machine or op is missing. */
29
+ export declare function readCommandFacets(md?: Record<string, any>): CommandFacets | null;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Commands as rows. A host asks a machine to start, stop, pause, resume,
4
+ * change over or cancel a job by parking a machine-command row on the machine's role, from a
5
+ * workflow step or through the API, the same way it asks a person for
6
+ * anything. The gateway sends the command and resolves the row with the
7
+ * machine's reply (CommandResolution), so the step reads the outcome as data.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.commandTarget = commandTarget;
11
+ exports.commandRow = commandRow;
12
+ exports.readCommandFacets = readCommandFacets;
13
+ const line_1 = require("../../lib/machines/line");
14
+ const contracts_1 = require("./contracts");
15
+ function commandTarget(line, machineId) {
16
+ return { siteId: line.siteId, lineId: line.lineId, machineId, role: (0, line_1.machineRole)((0, line_1.machineById)(line, machineId)) };
17
+ }
18
+ /** The row that asks a machine for `op`: pass it to conditional(), or create it through the API. */
19
+ function commandRow(target, op, args = {}) {
20
+ const metadata = { siteId: target.siteId, lineId: target.lineId, machineId: target.machineId, op, args };
21
+ const recipe = args.recipe;
22
+ return {
23
+ role: target.role,
24
+ type: contracts_1.ROW_TYPE.MACHINE_COMMAND,
25
+ subtype: op,
26
+ description: recipe?.id ? `${op} ${target.machineId} to ${recipe.id} v${recipe.version}` : `${op} ${target.machineId}`,
27
+ priority: 2,
28
+ metadata: metadata,
29
+ };
30
+ }
31
+ /** A machine-command row's facets; null when the line, machine or op is missing. */
32
+ function readCommandFacets(md = {}) {
33
+ if (!md.siteId || !md.lineId || !md.machineId || !md.op)
34
+ return null;
35
+ const args = md.args && typeof md.args === 'object' && !Array.isArray(md.args) ? md.args : {};
36
+ return { siteId: String(md.siteId), lineId: String(md.lineId), machineId: String(md.machineId), op: String(md.op), args };
37
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Environment for the machine line example. Every backend is chosen here and
3
+ * an unknown or incomplete selection throws; nothing falls back silently.
4
+ */
5
+ import type { MqttEndpoint } from '../../types/machines';
6
+ export type { MqttEndpoint };
7
+ export declare const MACHINE_TRANSPORT: {
8
+ readonly MEMORY: "memory";
9
+ readonly NATS: "nats";
10
+ readonly MQTT: "mqtt";
11
+ };
12
+ export type MachineTransportMode = typeof MACHINE_TRANSPORT[keyof typeof MACHINE_TRANSPORT];
13
+ export declare const ESCALATION_FEED: {
14
+ readonly MEMORY: "memory";
15
+ readonly CORE: "core";
16
+ readonly IN_PROCESS: "in-process";
17
+ };
18
+ export type EscalationFeedMode = typeof ESCALATION_FEED[keyof typeof ESCALATION_FEED];
19
+ export interface MachineLineConfig {
20
+ transport: MachineTransportMode;
21
+ feed: EscalationFeedMode;
22
+ topicRoot: string;
23
+ lineConfigPath?: string;
24
+ siteNatsUrl?: string;
25
+ siteNatsUser?: string;
26
+ siteNatsPassword?: string;
27
+ ltNatsUrl?: string;
28
+ ltNatsToken?: string;
29
+ mqtt?: MqttEndpoint;
30
+ replyTimeoutMs: number;
31
+ dispatchAttempts: number;
32
+ handoffTimeout: string;
33
+ offlineDebounceMs: number;
34
+ /** A connected machine silent this long gets a quiet row; 0 turns the check off. */
35
+ quietMs: number;
36
+ /** How long a dispatch stays valid at the machine. */
37
+ dispatchTtlMs: number;
38
+ /** How long confirmed message ids gather before a receipt goes out. */
39
+ receiptDelayMs: number;
40
+ /** Note each job's start, progress steps and pauses on the row that holds it (lifecycle.ts). */
41
+ lifecycle: boolean;
42
+ /** With lifecycle notes, progress is noted each time it crosses this many percent; 0 notes none. */
43
+ progressStep: number;
44
+ ltBaseUrl: string;
45
+ }
46
+ type Env = Record<string, string | undefined>;
47
+ /** The generic line protocol's topic root when MACHINE_TOPIC_ROOT is unset. */
48
+ export declare const DEFAULT_TOPIC_ROOT = "line/v1";
49
+ export declare function loadMachineLineConfig(env?: Env): MachineLineConfig;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * Environment for the machine line example. Every backend is chosen here and
4
+ * an unknown or incomplete selection throws; nothing falls back silently.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.DEFAULT_TOPIC_ROOT = exports.ESCALATION_FEED = exports.MACHINE_TRANSPORT = void 0;
8
+ exports.loadMachineLineConfig = loadMachineLineConfig;
9
+ const protocol_1 = require("../../lib/machines/protocol");
10
+ exports.MACHINE_TRANSPORT = { MEMORY: 'memory', NATS: 'nats', MQTT: 'mqtt' };
11
+ exports.ESCALATION_FEED = { MEMORY: 'memory', CORE: 'core', IN_PROCESS: 'in-process' };
12
+ /** The generic line protocol's topic root when MACHINE_TOPIC_ROOT is unset. */
13
+ exports.DEFAULT_TOPIC_ROOT = 'line/v1';
14
+ const SWITCH = { ON: 'on', OFF: 'off' };
15
+ function oneOf(name, value, allowed) {
16
+ if (!allowed.includes(value)) {
17
+ throw new Error(`${name}="${value}" is not one of ${allowed.join(', ')}`);
18
+ }
19
+ return value;
20
+ }
21
+ function int(env, name, fallback) {
22
+ const raw = env[name];
23
+ if (raw === undefined || raw === '')
24
+ return fallback;
25
+ const n = Number(raw);
26
+ if (!Number.isInteger(n) || n < 0)
27
+ throw new Error(`${name}="${raw}" must be a non-negative integer`);
28
+ return n;
29
+ }
30
+ function percentStep(env, name, fallback) {
31
+ const n = int(env, name, fallback);
32
+ if (n > 100)
33
+ throw new Error(`${name}="${n}" must be at most 100`);
34
+ return n;
35
+ }
36
+ function required(env, name, mode) {
37
+ const value = env[name];
38
+ if (!value)
39
+ throw new Error(`MACHINE_TRANSPORT=${mode} needs ${name}`);
40
+ return value;
41
+ }
42
+ function mqttEndpoint(env, mode, defaultVersion) {
43
+ const version = int(env, 'MACHINE_MQTT_PROTOCOL', defaultVersion);
44
+ if (version !== 4 && version !== 5)
45
+ throw new Error('MACHINE_MQTT_PROTOCOL must be 4 or 5');
46
+ return {
47
+ url: required(env, 'MACHINE_MQTT_URL', mode),
48
+ protocolVersion: version,
49
+ caFile: env.MACHINE_MQTT_CA,
50
+ certFile: env.MACHINE_MQTT_CERT,
51
+ keyFile: env.MACHINE_MQTT_KEY,
52
+ username: env.MACHINE_MQTT_USERNAME,
53
+ password: env.MACHINE_MQTT_PASSWORD,
54
+ };
55
+ }
56
+ const DEFAULT_FEED = {
57
+ memory: exports.ESCALATION_FEED.MEMORY,
58
+ nats: exports.ESCALATION_FEED.CORE,
59
+ mqtt: exports.ESCALATION_FEED.CORE,
60
+ };
61
+ function loadMachineLineConfig(env = process.env) {
62
+ const transport = oneOf('MACHINE_TRANSPORT', env.MACHINE_TRANSPORT ?? exports.MACHINE_TRANSPORT.MEMORY, Object.values(exports.MACHINE_TRANSPORT));
63
+ const feed = oneOf('MACHINE_ESCALATION_FEED', env.MACHINE_ESCALATION_FEED ?? DEFAULT_FEED[transport], Object.values(exports.ESCALATION_FEED));
64
+ const config = {
65
+ transport,
66
+ feed,
67
+ topicRoot: env.MACHINE_TOPIC_ROOT ?? exports.DEFAULT_TOPIC_ROOT,
68
+ lineConfigPath: env.MACHINE_LINE_CONFIG || undefined,
69
+ replyTimeoutMs: int(env, 'MACHINE_REPLY_TIMEOUT_MS', protocol_1.REPLY_DEADLINE_MS),
70
+ dispatchAttempts: Math.max(1, int(env, 'MACHINE_DISPATCH_ATTEMPTS', 3)),
71
+ handoffTimeout: env.MACHINE_HANDOFF_TIMEOUT ?? '10m',
72
+ offlineDebounceMs: int(env, 'MACHINE_OFFLINE_DEBOUNCE_MS', 15_000),
73
+ quietMs: int(env, 'MACHINE_QUIET_MS', 90_000),
74
+ dispatchTtlMs: int(env, 'MACHINE_DISPATCH_TTL_MS', 600_000),
75
+ receiptDelayMs: int(env, 'MACHINE_RECEIPT_DELAY_MS', 250),
76
+ lifecycle: oneOf('MACHINE_LIFECYCLE', env.MACHINE_LIFECYCLE || SWITCH.OFF, Object.values(SWITCH)) === SWITCH.ON,
77
+ progressStep: percentStep(env, 'MACHINE_PROGRESS_STEP', 25),
78
+ ltBaseUrl: env.LT_BASE_URL ?? 'http://localhost:3000',
79
+ };
80
+ if (transport === exports.MACHINE_TRANSPORT.NATS) {
81
+ // A site NATS of its own; without one, the streams are Long Tail's messaging.
82
+ config.siteNatsUrl = env.MACHINE_SITE_NATS_URL || undefined;
83
+ config.siteNatsUser = env.MACHINE_SITE_NATS_USER;
84
+ config.siteNatsPassword = env.MACHINE_SITE_NATS_PASSWORD;
85
+ config.mqtt = mqttEndpoint(env, transport, 5);
86
+ }
87
+ if (transport === exports.MACHINE_TRANSPORT.MQTT)
88
+ config.mqtt = mqttEndpoint(env, transport, 5);
89
+ if (feed === exports.ESCALATION_FEED.CORE) {
90
+ config.ltNatsUrl = env.NATS_URL || undefined;
91
+ config.ltNatsToken = env.NATS_TOKEN;
92
+ }
93
+ return config;
94
+ }