@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,210 @@
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
+ /** The metadata key an accumulator row parked with an item limit carries. */
33
+ const ACCUMULATE_MAX = 'accumulate_max';
34
+ 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];
35
+ function handoffRow(id, role, facets, data) {
36
+ const metadata = data?.metadata;
37
+ const capped = metadata?.[ACCUMULATE_MAX] != null;
38
+ const createdAt = data?.created_at ? new Date(data.created_at).getTime() : NaN;
39
+ return {
40
+ ...facets, id, role, dispatch: state_1.DISPATCH_STATE.NONE, trace: (0, trace_1.readTrace)(metadata),
41
+ ...(capped ? { capped } : {}), ...(Number.isFinite(createdAt) ? { createdAt } : {}),
42
+ };
43
+ }
44
+ /** Rows for another line belong to that line's gateway. */
45
+ function onThisLine(line, at) {
46
+ return at.siteId === line.siteId && at.lineId === line.lineId;
47
+ }
48
+ /** Whether an event names a row this line acts on; only then is the stored row read. */
49
+ function concernsLine(state, line, event, rows = rows_1.standardRows) {
50
+ if (!STATUS_FOR_VERB[event.verb])
51
+ return false;
52
+ if (state.get(event.escalationId))
53
+ return true;
54
+ const facets = rows.toHandoff({ ...event.data, id: event.escalationId, role: event.role });
55
+ if (facets)
56
+ return onThisLine(line, facets);
57
+ return LINE_ROW_TYPES.includes(event.data?.type) && onThisLine(line, event.data?.metadata ?? {});
58
+ }
59
+ /** Whether a stored row's status agrees with the event's verb, so a stale or invented event does nothing. */
60
+ function statusFits(verb, status) {
61
+ return STATUS_FOR_VERB[verb] === status;
62
+ }
63
+ /** A handoff into a machine is parked on that machine's role; one to a person, on one of the line's receiver roles. */
64
+ function handoffRoleFits(line, facets, role) {
65
+ if (onLine(line, facets.toMachine))
66
+ return role === (0, line_1.machineRole)((0, line_1.machineById)(line, facets.toMachine));
67
+ return (0, line_1.receiverRoles)(line).includes(role);
68
+ }
69
+ function planEscalationEvent(state, line, event, dispatchTtlMs, rows = rows_1.standardRows) {
70
+ const view = { ...event.data, id: event.escalationId, role: event.role };
71
+ const facets = rows.toHandoff(view);
72
+ if (facets)
73
+ return onThisLine(line, facets) && handoffRoleFits(line, facets, event.role) ? planHandoff(state, line, event, facets, dispatchTtlMs) : [];
74
+ if (state.get(event.escalationId))
75
+ return planHandoff(state, line, event, null, dispatchTtlMs);
76
+ const type = event.data?.type;
77
+ if (!onThisLine(line, event.data?.metadata ?? {}))
78
+ return [];
79
+ if (type === contracts_1.ROW_TYPE.ALARM)
80
+ return planAlarmRow(state, event);
81
+ if (type === contracts_1.ROW_TYPE.MACHINE_OFFLINE || type === contracts_1.ROW_TYPE.MACHINE_QUIET)
82
+ return planLinkRow(state, event);
83
+ if (type === contracts_1.ROW_TYPE.MACHINE_COMMAND)
84
+ return planCommandRow(state, line, event);
85
+ if (type === contracts_1.ROW_TYPE.MACHINE_REGISTRATION)
86
+ return event.role === (0, line_1.operatorRole)(line) ? (0, registration_1.planRegistrationRow)(state, line, event) : [];
87
+ return [];
88
+ }
89
+ /** Why the gateway will not send a command row's command; null when it will. */
90
+ function commandRefusal(state, line, c, role) {
91
+ if (!onLine(line, c.machineId))
92
+ return `${c.machineId} is not on ${line.siteId}/${line.lineId}`;
93
+ const own = (0, line_1.machineRole)((0, line_1.machineById)(line, c.machineId));
94
+ if (role !== own)
95
+ return `commands for ${c.machineId} are parked on ${own}, not ${role}`;
96
+ if (!protocol_1.CONTROL_OPS.includes(c.op))
97
+ return `${c.op} is not a command a row may ask for; one of ${protocol_1.CONTROL_OPS.join(', ')}`;
98
+ if (c.op === protocol_1.OP.CANCEL && typeof c.args.jobId !== 'string')
99
+ return `${protocol_1.OP.CANCEL} needs args.jobId, the job to withdraw or stop`;
100
+ if (!(0, registration_1.reachable)(state, line, c.machineId))
101
+ return `${c.machineId} has not registered an address yet`;
102
+ return null;
103
+ }
104
+ function planCommandRow(state, line, event) {
105
+ const id = event.escalationId;
106
+ if (event.verb === VERB.RESOLVED || event.verb === VERB.CANCELLED || event.verb === VERB.EXPIRED) {
107
+ state.endCommand(id);
108
+ return [];
109
+ }
110
+ if (event.verb !== VERB.CREATED && event.verb !== VERB.CLAIMED)
111
+ return [];
112
+ const c = (0, commands_1.readCommandFacets)(event.data?.metadata);
113
+ if (!c || !state.trackCommand(id))
114
+ return [];
115
+ const refusal = commandRefusal(state, line, c, event.role);
116
+ if (refusal) {
117
+ 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 } }];
118
+ }
119
+ const createdAt = event.data?.created_at ? new Date(event.data.created_at).getTime() : NaN;
120
+ return [{ kind: 'control', rowId: id, machineId: c.machineId, op: c.op, args: c.args, ...(Number.isFinite(createdAt) ? { createdAt } : {}) }];
121
+ }
122
+ function onLine(line, machineId) {
123
+ return !!machineId && line.machines.some((m) => m.machineId === machineId);
124
+ }
125
+ function heldUnder(state, machineId, unitId, senderJobId) {
126
+ const held = state.heldJob(machineId, unitId);
127
+ return !!held && (!senderJobId || (0, jobs_1.jobOwns)(senderJobId, held));
128
+ }
129
+ function planHandoff(state, line, event, facets, dispatchTtlMs) {
130
+ const id = event.escalationId;
131
+ if (event.verb === VERB.CREATED || event.verb === VERB.CLAIMED) {
132
+ if (state.isClosed(id) || !facets)
133
+ return [];
134
+ state.index(handoffRow(id, event.role, facets, event.data));
135
+ const row = state.get(id);
136
+ // The units now wait on the sender as this row's sender job, when the sender holds them under it.
137
+ if (row.fromMachine)
138
+ for (const u of row.units)
139
+ if (heldUnder(state, row.fromMachine, u.unitId, row.senderJobId))
140
+ state.letGo(row.fromMachine, u.unitId);
141
+ if (!onLine(line, row.toMachine) || row.dispatch !== state_1.DISPATCH_STATE.NONE)
142
+ return [];
143
+ // Read back already claimed: the machine accepted it before this gateway started.
144
+ if (event.data?.assigned_to) {
145
+ row.dispatch = state_1.DISPATCH_STATE.ACCEPTED;
146
+ return [];
147
+ }
148
+ if (state.dispatchOnArrival) {
149
+ const refused = (0, pacing_1.refuseUnfit)(state, line, row);
150
+ if (!refused.length)
151
+ row.dispatch = state_1.DISPATCH_STATE.ARRIVING;
152
+ return refused;
153
+ }
154
+ const refused = !!(0, pacing_1.softwareRefusal)(state, line, row.toMachine) || !!(0, pacing_1.jobRefusal)(state, row);
155
+ if (state.holdDispatch || (!refused && (!(0, pacing_1.hasRoom)(state, line, row) || !(0, registration_1.reachable)(state, line, row.toMachine)))) {
156
+ row.dispatch = state_1.DISPATCH_STATE.WAITING;
157
+ return [];
158
+ }
159
+ return (0, pacing_1.admit)(state, line, row, dispatchTtlMs);
160
+ }
161
+ if (event.verb === VERB.CANCELLED || event.verb === VERB.EXPIRED) {
162
+ const row = state.close(id, event.verb === VERB.CANCELLED ? state_1.ROW_END.CANCELLED : state_1.ROW_END.EXPIRED);
163
+ return row ? (0, plan_shared_1.cancelDispatch)(state, row) : [];
164
+ }
165
+ if (event.verb === VERB.RESOLVED)
166
+ state.close(id);
167
+ return [];
168
+ }
169
+ function planAlarmRow(state, event) {
170
+ const md = event.data.metadata ?? {};
171
+ const track = state.alarms.get(state.alarmKey(md.machineId, md.alarmId));
172
+ if (event.verb === VERB.CREATED) {
173
+ // A row for an earlier raise of the same alarm id is not this raise's row.
174
+ if (track && !track.rowId && (!track.raisedAt || !md.raisedAt || track.raisedAt === md.raisedAt))
175
+ track.rowId = event.escalationId;
176
+ return [];
177
+ }
178
+ // Only the row of the raise still tracked speaks for it; an older row ending acknowledges nothing.
179
+ if (!track || track.rowId !== event.escalationId || (event.verb !== VERB.RESOLVED && event.verb !== VERB.CANCELLED))
180
+ return [];
181
+ track.open = false;
182
+ if (event.verb !== VERB.RESOLVED)
183
+ return [];
184
+ // Still tracked means the machine has not cleared it: a person acknowledged it.
185
+ return [{
186
+ kind: 'command', machineId: track.machineId, op: protocol_1.OP.ACKNOWLEDGE_ALARM, cmdId: `${event.escalationId}:ack`,
187
+ args: { alarmId: track.alarmId, by: event.data.resolved_by ?? 'line-operator' },
188
+ }];
189
+ }
190
+ function planLinkRow(state, event) {
191
+ const machineId = event.data.metadata?.machineId;
192
+ const link = machineId ? state.links.get(machineId) : undefined;
193
+ if (!link)
194
+ return [];
195
+ const quiet = event.data.type === contracts_1.ROW_TYPE.MACHINE_QUIET;
196
+ const ended = event.verb === VERB.RESOLVED || event.verb === VERB.CANCELLED;
197
+ if (quiet) {
198
+ if (event.verb === VERB.CREATED)
199
+ link.quietRowId = event.escalationId;
200
+ if (ended && link.quietRowId === event.escalationId)
201
+ link.quietRowId = undefined;
202
+ }
203
+ else {
204
+ if (event.verb === VERB.CREATED)
205
+ link.rowId = event.escalationId;
206
+ if (ended && link.rowId === event.escalationId)
207
+ link.rowId = undefined;
208
+ }
209
+ return [];
210
+ }
@@ -0,0 +1,17 @@
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. A job's
6
+ * start, its progress steps, and the alarms that pause it are noted on the row
7
+ * that holds it (lifecycle.ts). Alarms that stop work and machines that go
8
+ * dark or quiet become rows for people. A restarted machine gets back the
9
+ * dispatches it lost. Everything else stays in the stream.
10
+ *
11
+ * A message whose row does not exist yet is nak'd and arrives again; one whose
12
+ * row already closed is acked, so a replay never resolves anything twice.
13
+ */
14
+ import { type Envelope, type ParsedTopic } from '../../../lib/machines/protocol';
15
+ import { type Plan } from './actions';
16
+ import { type GatewayState } from './state';
17
+ export declare function planMachineMessage(state: GatewayState, parsed: ParsedTopic, env: Envelope | null, dispatchTtlMs: number): Plan;
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ /**
3
+ * Machine messages to gateway actions: the escalation verbs a person would
4
+ * perform, performed for the machine. Each unit's arrival and the sender's
5
+ * finished record accumulate on the handoff row, and the last expected item
6
+ * resolves it. A failure on either side resolves it with an error. A job's
7
+ * start, its progress steps, and the alarms that pause it are noted on the row
8
+ * that holds it (lifecycle.ts). Alarms that stop work and machines that go
9
+ * dark or quiet become rows for people. A restarted machine gets back the
10
+ * dispatches it lost. Everything else stays in the stream.
11
+ *
12
+ * A message whose row does not exist yet is nak'd and arrives again; one whose
13
+ * row already closed is acked, so a replay never resolves anything twice.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.planMachineMessage = planMachineMessage;
17
+ const contracts_1 = require("../contracts");
18
+ const protocol_1 = require("../../../lib/machines/protocol");
19
+ const transport_1 = require("../../../lib/machines/transport");
20
+ const escalation_1 = require("../../../types/escalation");
21
+ const actions_1 = require("./actions");
22
+ const pacing_1 = require("./pacing");
23
+ const plan_shared_1 = require("./plan-shared");
24
+ const jobs_1 = require("./jobs");
25
+ const lifecycle_1 = require("./lifecycle");
26
+ const trace_1 = require("./trace");
27
+ const state_1 = require("./state");
28
+ const ack = (actions = []) => ({ actions, disposition: transport_1.DISPOSITION.ACK });
29
+ const nak = { actions: [], disposition: transport_1.DISPOSITION.NAK };
30
+ function planMachineMessage(state, parsed, env, dispatchTtlMs) {
31
+ const m = parsed.ident.machineId;
32
+ const b = env?.body ?? {};
33
+ const plan = planByKind(state, m, parsed, env, b, dispatchTtlMs);
34
+ return { ...plan, actions: [...heard(state, m), ...plan.actions] };
35
+ }
36
+ function planByKind(state, m, parsed, env, b, ttl) {
37
+ switch (parsed.kind) {
38
+ case protocol_1.MESSAGE_KIND.REPLY:
39
+ return ack([{ kind: 'settleReply', cmdId: b.cmdId, status: b.status, reason: b.reason }]);
40
+ case protocol_1.MESSAGE_KIND.JOB:
41
+ return planJob(state, m, b, env?.at, ttl);
42
+ case protocol_1.MESSAGE_KIND.ALARM:
43
+ return planAlarm(state, parsed, parsed.key ?? b.alarmId, env);
44
+ case protocol_1.MESSAGE_KIND.PRESENCE:
45
+ return planPresence(state, m, b);
46
+ case protocol_1.MESSAGE_KIND.SNAPSHOT:
47
+ return planSnapshot(state, m, b, ttl);
48
+ default:
49
+ return ack();
50
+ }
51
+ }
52
+ /** Any message proves a quiet machine is talking again. */
53
+ function heard(state, m) {
54
+ const link = state.link(m);
55
+ link.lastSeen = Date.now();
56
+ if (!link.quietRowId)
57
+ return [];
58
+ const rowId = link.quietRowId;
59
+ link.quietRowId = undefined;
60
+ return [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId, payload: { decision: contracts_1.DECISION.RESTORED, note: `${m} is talking again` } }];
61
+ }
62
+ /** Ack when the matching row already closed (a replay or a cancel); otherwise wait for the row. */
63
+ function missing(state, unitId, pred) {
64
+ return state.closedFor(unitId, pred) ? ack() : nak;
65
+ }
66
+ /** Whether a machine's job id belongs to `jobId`; a row for some of a job's units still owns it. */
67
+ function ownsJob(jobId, reported) {
68
+ if (!jobId || typeof reported !== 'string')
69
+ return true;
70
+ return (0, jobs_1.jobOwns)(jobId, reported);
71
+ }
72
+ /** Keep the receiver's arrival event for the unit, to go out with its dispatch. */
73
+ function withArrival(row, unitId, b, at) {
74
+ row.arrivals = { ...row.arrivals, [unitId]: b };
75
+ row.dueAt = (0, pacing_1.latestArrival)(row.dueAt, at);
76
+ return row;
77
+ }
78
+ /** The unit an open handoff into `m` carries on `carrierId`, for a scan that read only the carrier. */
79
+ function unitOnCarrier(state, m, carrierId) {
80
+ if (typeof carrierId !== 'string')
81
+ return undefined;
82
+ for (const r of state.open()) {
83
+ if (r.toMachine !== m)
84
+ continue;
85
+ const unit = r.units.find((u) => u.carrierId === carrierId);
86
+ if (unit)
87
+ return unit.unitId;
88
+ }
89
+ return undefined;
90
+ }
91
+ function planJob(state, m, b, at, ttl) {
92
+ const unitId = typeof b.unitId === 'string' ? b.unitId : unitOnCarrier(state, m, b.carrierId);
93
+ // A carrier no open handoff here carries yet: its row may still be on the way.
94
+ if (unitId === undefined)
95
+ return nak;
96
+ // An event from an earlier attempt's job never touches a later attempt's row.
97
+ const toHere = (r) => r.toMachine === m && ownsJob(r.jobId, b.jobId);
98
+ const fromHere = (r) => r.fromMachine === m && ownsJob(r.senderJobId, b.jobId);
99
+ if (b.event === protocol_1.JOB_EVENT.ARRIVED) {
100
+ const row = state.openFor(unitId, toHere);
101
+ if (!row)
102
+ return missing(state, unitId, toHere);
103
+ const expected = row.units.find((u) => u.unitId === unitId)?.carrierId;
104
+ if (expected && b.carrierId && expected !== b.carrierId) {
105
+ return ack((0, plan_shared_1.failHandoff)(state, row, m, {
106
+ received: false, unitId, errorCode: contracts_1.HANDOFF_ERROR.CARRIER_MISMATCH, detail: `${m} read carrier ${b.carrierId}, expected ${expected}`,
107
+ }));
108
+ }
109
+ // The unit is at the receiver now, and off the sender.
110
+ const job = (0, jobs_1.dispatchJobs)(row.jobId, row.units, state.unitsPerDispatch).find((j) => j.units.some((u) => u.unitId === unitId));
111
+ if (job)
112
+ state.hold(m, unitId, job.jobId);
113
+ const early = row.fromMachine ? releasedEarly(state, row, unitId) : [];
114
+ if (row.fromMachine)
115
+ state.letGo(row.fromMachine, unitId);
116
+ const units = row.units.map((u) => u.unitId);
117
+ const arrivedAt = at ?? new Date().toISOString();
118
+ const complete = row.dispatch === state_1.DISPATCH_STATE.ARRIVING
119
+ ? { keys: units.map(contracts_1.itemKeys.arrived), then: [(0, pacing_1.dispatchAction)(withArrival(row, unitId, b, arrivedAt), m, ttl)] }
120
+ : { keys: units.map(contracts_1.itemKeys.arrived), payload: { received: true, receiver: m, units } };
121
+ return ack([...early, {
122
+ kind: 'accumulate', as: m, rowId: row.id, itemKey: contracts_1.itemKeys.arrived(unitId),
123
+ payload: {
124
+ unitId, carrierId: b.carrierId ?? null, from: b.peer ?? null, at: arrivedAt,
125
+ ...(row.dispatch === state_1.DISPATCH_STATE.ARRIVING ? { arrival: b } : {}),
126
+ },
127
+ complete,
128
+ }]);
129
+ }
130
+ if (b.event === protocol_1.JOB_EVENT.FINISHED) {
131
+ const row = state.openFor(unitId, fromHere);
132
+ if (!row) {
133
+ if (!state.closedFor(unitId, fromHere))
134
+ state.rememberFinish(m, unitId, b);
135
+ return missing(state, unitId, fromHere);
136
+ }
137
+ state.takeFinish(m, unitId);
138
+ const released = releasedAction(row, m, b);
139
+ if (b.result === protocol_1.JOB_RESULT.OK)
140
+ return ack([released]);
141
+ return ack([released, ...(0, plan_shared_1.failHandoff)(state, row, m, {
142
+ received: false, unitId, errorCode: contracts_1.HANDOFF_ERROR.SENDER_REJECTED, reason: b.reason ?? null, detail: `${m} finished ${unitId}: ${b.result}${b.reason ? ` (${b.reason})` : ''}`,
143
+ })]);
144
+ }
145
+ if (b.event === protocol_1.JOB_EVENT.TRANSFER_FAILED) {
146
+ const either = (r) => toHere(r) || fromHere(r);
147
+ const row = state.openFor(unitId, either);
148
+ if (!row)
149
+ return missing(state, unitId, either);
150
+ return ack((0, plan_shared_1.failHandoff)(state, row, m, { received: false, unitId, errorCode: b.failure, detail: `${m} reported ${b.failure}` }));
151
+ }
152
+ if (b.event === protocol_1.JOB_EVENT.RELEASED) {
153
+ state.letGo(m, unitId);
154
+ return ack();
155
+ }
156
+ if (b.event === protocol_1.JOB_EVENT.WITHDRAWN) {
157
+ state.letGo(m, unitId);
158
+ const row = state.openFor(unitId, toHere);
159
+ if (!row)
160
+ return ack();
161
+ return ack((0, plan_shared_1.failHandoff)(state, row, m, { received: false, unitId, errorCode: contracts_1.HANDOFF_ERROR.WITHDRAWN, detail: `${m} gave up ${b.jobId}` }));
162
+ }
163
+ if (b.event === protocol_1.JOB_EVENT.STARTED || b.event === protocol_1.JOB_EVENT.PROGRESS)
164
+ return ack((0, lifecycle_1.noteJobEvent)(state, m, b, at));
165
+ return ack();
166
+ }
167
+ function releasedAction(row, m, b) {
168
+ row.trace = (0, trace_1.mergeTrace)(row.trace, [(0, trace_1.traceEntry)(m, b)]);
169
+ return { kind: 'accumulate', as: m, rowId: row.id, itemKey: contracts_1.itemKeys.released(b.unitId), payload: b, metadata: (0, trace_1.traceFacets)(row.trace) };
170
+ }
171
+ /** The sender's finished record that came before the row, written ahead of the arrival that may resolve it. */
172
+ function releasedEarly(state, row, unitId) {
173
+ const b = state.takeFinish(row.fromMachine, unitId);
174
+ if (!b || b.result !== protocol_1.JOB_RESULT.OK || !ownsJob(row.senderJobId, b.jobId))
175
+ return [];
176
+ return [releasedAction(row, row.fromMachine, b)];
177
+ }
178
+ function planAlarm(state, parsed, alarmId, env) {
179
+ const { siteId, lineId, machineId: m } = parsed.ident;
180
+ const key = state.alarmKey(m, alarmId);
181
+ const b = env?.body ?? {};
182
+ if (!env || b.active === false) {
183
+ const track = state.alarms.get(key);
184
+ state.alarms.delete(key);
185
+ const resumed = track ? (0, lifecycle_1.noteResume)(state, track, b) : [];
186
+ if (!track?.rowId || !track.open)
187
+ return ack(resumed);
188
+ return ack([...resumed, {
189
+ kind: 'resolve', as: actions_1.CONTROLLER, rowId: track.rowId,
190
+ payload: { decision: contracts_1.DECISION.CLEARED, clearedBy: b.clearedBy ?? null, note: `cleared at ${m}${b.clearedBy ? ` by ${b.clearedBy}` : ''}` },
191
+ }]);
192
+ }
193
+ if (!protocol_1.STOPPING_LEVELS.includes(b.level))
194
+ return ack();
195
+ const known = state.alarms.get(key);
196
+ if (known && (!known.raisedAt || !b.raisedAt || known.raisedAt === b.raisedAt))
197
+ return ack();
198
+ const superseded = known?.rowId && known.open
199
+ ? [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: known.rowId, payload: { decision: contracts_1.DECISION.CLEARED, note: `${m} raised ${alarmId} again; this one ended` } }]
200
+ : [];
201
+ const track = { machineId: m, alarmId, raisedAt: b.raisedAt, open: true };
202
+ state.alarms.set(key, track);
203
+ return ack([...superseded, ...(0, lifecycle_1.notePause)(state, track, b), {
204
+ kind: 'createAlarmRow', machineId: m, alarmId,
205
+ row: {
206
+ type: contracts_1.ROW_TYPE.ALARM, subtype: b.level, role: contracts_1.LINE_ROLE.OPERATOR, priority: b.level === protocol_1.LEVEL.EMERGENCY ? 1 : 2,
207
+ description: `${m}: ${b.text}`,
208
+ metadata: {
209
+ siteId, lineId, machineId: m, alarmId, code: b.code, level: b.level, raisedAt: b.raisedAt,
210
+ zone: b.zone ?? null, autoClears: b.autoClears ?? false, action: b.action ?? null, jobId: b.jobId ?? null,
211
+ },
212
+ ...catalogEnvelope(b.catalog),
213
+ },
214
+ }]);
215
+ }
216
+ /** An alarm naming the catalog edition its code comes from carries that edition as a lookup, so the row reads its meaning. */
217
+ function catalogEnvelope(catalog) {
218
+ const ref = catalog;
219
+ if (!ref || typeof ref.domain !== 'string' || typeof ref.key !== 'string')
220
+ return {};
221
+ return { envelope: JSON.stringify({ [escalation_1.ESCALATION_ENVELOPE_KEYS.LOOKUPS]: [ref] }) };
222
+ }
223
+ function planPresence(state, m, b) {
224
+ const link = state.link(m);
225
+ if (b.online !== true) {
226
+ if (link.online)
227
+ link.since = Date.now();
228
+ link.online = false;
229
+ if (link.rowId || link.watching)
230
+ return ack();
231
+ link.watching = true;
232
+ return ack([{ kind: 'watchOffline', machineId: m }]);
233
+ }
234
+ if (b.software)
235
+ link.software = b.software;
236
+ const wasOffline = !link.online;
237
+ link.online = true;
238
+ link.watching = false;
239
+ const actions = [];
240
+ // Back from a drop: ask for the full state rather than wait for the next heartbeat.
241
+ if (wasOffline)
242
+ actions.push({ kind: 'command', machineId: m, op: protocol_1.OP.SNAPSHOT, cmdId: `${m}:resync:${link.since}`, args: {} });
243
+ if (link.rowId) {
244
+ actions.push({ kind: 'resolve', as: actions_1.CONTROLLER, rowId: link.rowId, payload: { decision: contracts_1.DECISION.RESTORED, note: `${m} back online` } });
245
+ link.rowId = undefined;
246
+ }
247
+ return ack(actions);
248
+ }
249
+ /** A new boot id means the machine restarted and lost its queue: send its open dispatches again. */
250
+ function planSnapshot(state, m, b, ttl) {
251
+ const link = state.link(m);
252
+ if (b.software)
253
+ link.software = b.software;
254
+ const jobIds = Array.isArray(b.jobs) ? b.jobs.map((j) => j?.jobId).filter((id) => typeof id === 'string') : [];
255
+ if (Array.isArray(b.jobs))
256
+ state.keepHeld(m, new Set(jobIds));
257
+ const fresh = !state.retainedAlarms && !state.readingRetained && Array.isArray(b.alarms);
258
+ const cleared = fresh ? alarmsNoLongerListed(state, m, b.alarms) : [];
259
+ const answer = { kind: 'settleSnapshot', machineId: m, jobIds };
260
+ const previous = link.bootId;
261
+ link.bootId = b.bootId;
262
+ if (!previous || previous === b.bootId)
263
+ return ack([...cleared, answer]);
264
+ const held = new Set(jobIds);
265
+ const missing = (r) => (0, jobs_1.dispatchJobs)(r.jobId, r.units, state.unitsPerDispatch).some((j) => !held.has(j.jobId));
266
+ const lost = state.open().filter((r) => r.toMachine === m && r.dispatch === state_1.DISPATCH_STATE.ACCEPTED && missing(r));
267
+ return ack([...cleared, answer, ...lost.map((r) => (0, pacing_1.dispatchAction)(r, m, ttl))]);
268
+ }
269
+ /** Alarm rows for `m` whose alarm its snapshot no longer lists: they cleared at the machine. */
270
+ function alarmsNoLongerListed(state, m, listed) {
271
+ const active = new Set(listed.filter((id) => typeof id === 'string'));
272
+ const actions = [];
273
+ for (const [key, track] of [...state.alarms.entries()]) {
274
+ if (track.machineId !== m || active.has(track.alarmId))
275
+ continue;
276
+ state.alarms.delete(key);
277
+ actions.push(...(0, lifecycle_1.noteResume)(state, track, {}));
278
+ if (track.open && track.rowId) {
279
+ actions.push({ kind: 'resolve', as: actions_1.CONTROLLER, rowId: track.rowId, payload: { decision: contracts_1.DECISION.CLEARED, note: `cleared at ${m}; its snapshot no longer lists it` } });
280
+ }
281
+ }
282
+ return actions;
283
+ }
@@ -0,0 +1,18 @@
1
+ import { type HandoffResolution } from '../contracts';
2
+ import type { Action, Actor } from './actions';
3
+ import { type DispatchJob } from './jobs';
4
+ import { type GatewayState, type HandoffRow } from './state';
5
+ /** A cancel for each job a handoff's units became at `machineId`, or for those `keep` leaves out. */
6
+ export declare function cancelJobs(state: GatewayState, row: HandoffRow, machineId: string, jobId: string, tag?: string, keep?: (job: DispatchJob) => boolean): Action[];
7
+ /**
8
+ * Withdraw a dispatch that may still sit in the receiver's queue. A job some
9
+ * of whose units already reached the receiver is under way: it stays, and
10
+ * what happens to it is the workflow's next decision (a machine-command row).
11
+ */
12
+ export declare function cancelDispatch(state: GatewayState, row: HandoffRow): Action[];
13
+ /**
14
+ * End a handoff without a receipt: resolve it with the error, then withdraw the
15
+ * dispatch. A row the receiver has claimed is resolved by the receiver (only the
16
+ * claimant may resolve a claimed row); `reportedBy` names who saw the failure.
17
+ */
18
+ export declare function failHandoff(state: GatewayState, row: HandoffRow, reporter: Actor, resolution: HandoffResolution): Action[];
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cancelJobs = cancelJobs;
4
+ exports.cancelDispatch = cancelDispatch;
5
+ exports.failHandoff = failHandoff;
6
+ const contracts_1 = require("../contracts");
7
+ const protocol_1 = require("../../../lib/machines/protocol");
8
+ const jobs_1 = require("./jobs");
9
+ const state_1 = require("./state");
10
+ /** A cancel for each job a handoff's units became at `machineId`, or for those `keep` leaves out. */
11
+ function cancelJobs(state, row, machineId, jobId, tag = '', keep = () => false) {
12
+ return (0, jobs_1.dispatchJobs)(jobId, row.units, state.unitsPerDispatch).filter((job) => !keep(job)).map((job) => ({
13
+ kind: 'command', machineId, op: protocol_1.OP.CANCEL, cmdId: `${(0, contracts_1.cancelCommandId)(row.id)}${tag}${job.suffix}`, args: { jobId: job.jobId },
14
+ }));
15
+ }
16
+ /**
17
+ * Withdraw a dispatch that may still sit in the receiver's queue. A job some
18
+ * of whose units already reached the receiver is under way: it stays, and
19
+ * what happens to it is the workflow's next decision (a machine-command row).
20
+ */
21
+ function cancelDispatch(state, row) {
22
+ const to = row.toMachine;
23
+ if (!to || row.dispatch === state_1.DISPATCH_STATE.NONE || row.dispatch === state_1.DISPATCH_STATE.REFUSED)
24
+ return [];
25
+ return cancelJobs(state, row, to, row.jobId, '', (job) => job.units.some((u) => state.heldJob(to, u.unitId) === job.jobId));
26
+ }
27
+ /**
28
+ * End a handoff without a receipt: resolve it with the error, then withdraw the
29
+ * dispatch. A row the receiver has claimed is resolved by the receiver (only the
30
+ * claimant may resolve a claimed row); `reportedBy` names who saw the failure.
31
+ */
32
+ function failHandoff(state, row, reporter, resolution) {
33
+ const claimant = row.dispatch === state_1.DISPATCH_STATE.ACCEPTED && row.toMachine ? row.toMachine : reporter;
34
+ const payload = claimant === reporter ? { ...resolution } : { ...resolution, reportedBy: reporter };
35
+ return [{ kind: 'resolve', as: claimant, rowId: row.id, payload }, ...cancelDispatch(state, row)];
36
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Identities. Each machine acts in Long Tail as its own bot account, so the
3
+ * rows it claims and resolves name the machine beside the people who worked
4
+ * the unit; the gateway's own bot (the controller) raises operator rows. At
5
+ * boot the gateway logs in, makes sure every bot exists with its roles, and
6
+ * mints a fresh session key per bot, revoking the previous one.
7
+ */
8
+ import { type LineDef } from '../../../lib/machines/line';
9
+ import { type Actor } from './actions';
10
+ import type { LtHttp } from './lt-http';
11
+ export declare const SESSION_KEY_NAME = "machine-gateway-session";
12
+ export interface Identity {
13
+ actor: Actor;
14
+ botName: string;
15
+ roles: string[];
16
+ }
17
+ /** The bots a line needs: one per machine, plus the controller. */
18
+ export declare function lineIdentities(line: LineDef): Identity[];
19
+ /** A builder token: LT_GATEWAY_TOKEN when set, otherwise a username/password login. */
20
+ export declare function adminToken(http: LtHttp, env: Record<string, string | undefined>): Promise<string>;
21
+ /** Ensure every bot and mint its session key; returns actor → bearer token. */
22
+ export declare function provisionIdentities(http: LtHttp, admin: string, line: LineDef): Promise<Map<Actor, string>>;