@hotmeshio/long-tail 0.28.0 → 0.28.1

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 (88) hide show
  1. package/build/api/files.d.ts +5 -0
  2. package/build/api/files.js +19 -2
  3. package/build/api/knowledge.d.ts +3 -1
  4. package/build/lib/db/schemas/042_knowledge_edition_labels.sql +9 -0
  5. package/build/lib/machines/line.d.ts +3 -0
  6. package/build/lib/machines/line.js +7 -1
  7. package/build/lib/machines/protocol/codec.d.ts +6 -0
  8. package/build/lib/machines/protocol/codec.js +1 -1
  9. package/build/lib/machines/protocol/contract.d.ts +2 -0
  10. package/build/lib/machines/protocol/contract.js +12 -7
  11. package/build/lib/machines/site/mosquitto-conf.d.ts +7 -3
  12. package/build/lib/machines/site/mosquitto-conf.js +3 -8
  13. package/build/lib/machines/site/site-conf.d.ts +4 -1
  14. package/build/lib/machines/site/site-conf.js +5 -1
  15. package/build/lib/machines/transport/nats-bus.d.ts +2 -0
  16. package/build/lib/machines/transport/stream-bus.d.ts +5 -1
  17. package/build/lib/machines/transport/stream-bus.js +12 -4
  18. package/build/lib/machines/transport/streams.d.ts +10 -2
  19. package/build/lib/machines/transport/streams.js +32 -5
  20. package/build/lib/messaging/memory.js +3 -0
  21. package/build/lib/messaging/nats/streams.d.ts +3 -0
  22. package/build/lib/messaging/nats/streams.js +16 -0
  23. package/build/lib/storage/local.d.ts +2 -0
  24. package/build/lib/storage/local.js +3 -0
  25. package/build/lib/storage/s3.d.ts +3 -2
  26. package/build/lib/storage/s3.js +6 -1
  27. package/build/lib/storage/types.d.ts +7 -1
  28. package/build/routes/file-browser.js +3 -1
  29. package/build/routes/knowledge.js +6 -5
  30. package/build/routes/machines.js +33 -0
  31. package/build/services/knowledge/index.d.ts +4 -2
  32. package/build/services/knowledge/index.js +8 -2
  33. package/build/services/knowledge/lookup-cache.d.ts +8 -0
  34. package/build/services/knowledge/lookup-cache.js +30 -3
  35. package/build/services/knowledge/lookup-refs.js +1 -1
  36. package/build/services/knowledge/sql.d.ts +4 -2
  37. package/build/services/knowledge/sql.js +8 -3
  38. package/build/services/machines/config.d.ts +2 -0
  39. package/build/services/machines/config.js +1 -0
  40. package/build/services/machines/contracts.d.ts +18 -0
  41. package/build/services/machines/contracts.js +15 -1
  42. package/build/services/machines/forms.d.ts +26 -1
  43. package/build/services/machines/forms.js +17 -1
  44. package/build/services/machines/gateway/actions.d.ts +13 -1
  45. package/build/services/machines/gateway/admission.d.ts +15 -1
  46. package/build/services/machines/gateway/admission.js +47 -0
  47. package/build/services/machines/gateway/api-escalations.js +4 -0
  48. package/build/services/machines/gateway/dispatcher.d.ts +9 -4
  49. package/build/services/machines/gateway/dispatcher.js +42 -14
  50. package/build/services/machines/gateway/executor.d.ts +2 -0
  51. package/build/services/machines/gateway/executor.js +18 -2
  52. package/build/services/machines/gateway/gateway.d.ts +5 -0
  53. package/build/services/machines/gateway/gateway.js +11 -3
  54. package/build/services/machines/gateway/log.d.ts +2 -0
  55. package/build/services/machines/gateway/log.js +2 -0
  56. package/build/services/machines/gateway/lt-http.js +11 -2
  57. package/build/services/machines/gateway/pacing.js +1 -1
  58. package/build/services/machines/gateway/plan-machine.js +26 -11
  59. package/build/services/machines/gateway/plan-shared.d.ts +5 -3
  60. package/build/services/machines/gateway/plan-shared.js +6 -4
  61. package/build/services/machines/gateway/reply-waiter.d.ts +2 -0
  62. package/build/services/machines/gateway/state.d.ts +24 -1
  63. package/build/services/machines/gateway/state.js +67 -6
  64. package/build/services/machines/gateway/wire-window.d.ts +21 -0
  65. package/build/services/machines/gateway/wire-window.js +61 -0
  66. package/build/services/machines/gateway/wire.js +1 -0
  67. package/build/services/machines/roles.d.ts +5 -4
  68. package/build/services/machines/roles.js +13 -4
  69. package/build/services/machines/select.d.ts +3 -2
  70. package/build/services/machines/select.js +4 -3
  71. package/build/services/machines/surface.d.ts +10 -1
  72. package/build/services/machines/surface.js +20 -4
  73. package/build/system/activities/knowledge.d.ts +4 -0
  74. package/build/system/activities/knowledge.js +15 -0
  75. package/build/system/activities/sql.d.ts +2 -0
  76. package/build/system/activities/sql.js +6 -1
  77. package/build/system/mcp-servers/knowledge.js +4 -2
  78. package/build/tsconfig.tsbuildinfo +1 -1
  79. package/build/types/escalation.d.ts +9 -6
  80. package/build/types/escalation.js +8 -2
  81. package/build/types/machines-view.d.ts +16 -0
  82. package/build/types/machines.d.ts +1 -0
  83. package/build/types/messaging.d.ts +2 -0
  84. package/build/types/startup.d.ts +2 -0
  85. package/docs/dashboard.md +1 -1
  86. package/docs/hitl/lookups.md +6 -4
  87. package/docs/machines.md +25 -16
  88. package/package.json +1 -1
@@ -128,6 +128,7 @@ router.get('/download/{*filePath}', auth_1.requireBuilder, async (req, res) => {
128
128
  * POST /api/file-browser/upload
129
129
  * Upload a file to storage.
130
130
  * Query: ?path=images/photo.png (target path including filename)
131
+ * &sha256=<hex> (optional; the upload is refused with 422 when the bytes differ)
131
132
  * Body: raw file bytes (Content-Type should match the file type)
132
133
  */
133
134
  router.post('/upload', auth_1.requireBuilder, async (req, res) => {
@@ -146,7 +147,8 @@ router.post('/upload', auth_1.requireBuilder, async (req, res) => {
146
147
  res.status(400).json({ error: 'Empty file body' });
147
148
  return;
148
149
  }
149
- const result = await api.uploadFile({ path: targetPath, buffer });
150
+ const sha256 = typeof req.query.sha256 === 'string' ? req.query.sha256 : undefined;
151
+ const result = await api.uploadFile({ path: targetPath, buffer, sha256 });
150
152
  res.status(result.status).json(result.data ?? { error: result.error });
151
153
  }
152
154
  catch (err) {
@@ -74,8 +74,8 @@ router.get('/entries', async (req, res) => {
74
74
  /**
75
75
  * GET /api/knowledge/entry
76
76
  * Get a single knowledge entry — the live edition, or an immutable snapshot
77
- * when ?version=N is present.
78
- * Query: ?domain=...&key=...&version=N
77
+ * when ?version=N (or ?version=<label>) is present.
78
+ * Query: ?domain=...&key=...&version=N|label
79
79
  */
80
80
  router.get('/entry', async (req, res) => {
81
81
  const domain = req.query.domain;
@@ -85,9 +85,10 @@ router.get('/entry', async (req, res) => {
85
85
  return;
86
86
  }
87
87
  if (req.query.version !== undefined) {
88
- const version = Number(req.query.version);
89
- if (!Number.isInteger(version) || version < 1) {
90
- res.status(400).json({ error: 'version must be a positive integer' });
88
+ const raw = String(req.query.version);
89
+ const version = /^[1-9][0-9]*$/.test(raw) ? Number(raw) : raw;
90
+ if (version === '' || version === 'current') {
91
+ res.status(400).json({ error: 'version must be a positive integer or an edition label' });
91
92
  return;
92
93
  }
93
94
  const result = await api.getEntryVersion({ domain, key, version });
@@ -42,6 +42,8 @@ const registry_1 = require("../services/machines/registry");
42
42
  const registry_sync_1 = require("../services/machines/registry-sync");
43
43
  const line_index_1 = require("../services/machines/line-index");
44
44
  const topics_1 = require("../lib/machines/protocol/topics");
45
+ const browser_view_1 = require("../lib/events/browser-view");
46
+ const wire_window_1 = require("../services/machines/gateway/wire-window");
45
47
  const router = (0, http_1.Router)();
46
48
  /**
47
49
  * The machine surface for the dashboard's Lines view. Reads and the dry run
@@ -170,6 +172,37 @@ router.get('/lines/:siteId/:lineId', auth_1.requireRoleManager, (req, res) => {
170
172
  };
171
173
  res.status(200).json(body);
172
174
  });
175
+ /** The event as this viewer reads it: builders see the payload, everyone else the record without it. */
176
+ function wireForViewer(event, builder) {
177
+ if (builder)
178
+ return event;
179
+ const { payload: _payload, ...rest } = event;
180
+ return rest;
181
+ }
182
+ router.get('/lines/:siteId/:lineId/wire', auth_1.requireRoleManager, async (req, res) => {
183
+ const { surface, siteId, lineId, line } = lineOf(req);
184
+ if (!surface || !line) {
185
+ res.status(404).json({ error: surface ? `no line ${siteId}/${lineId}` : NO_LINES });
186
+ return;
187
+ }
188
+ const { direction, kind } = req.query;
189
+ if (direction !== undefined && !(0, wire_window_1.isWireDirection)(direction)) {
190
+ res.status(400).json({ error: 'direction must be in or out' });
191
+ return;
192
+ }
193
+ if (kind !== undefined && !(0, wire_window_1.isWireKind)(kind)) {
194
+ res.status(400).json({ error: 'kind is not a wire kind' });
195
+ return;
196
+ }
197
+ const query = {
198
+ limit: queryInt(req.query.limit), machine: queryString(req.query.machine), since: queryString(req.query.since),
199
+ ...(direction !== undefined ? { direction } : {}), ...(kind !== undefined ? { kind } : {}),
200
+ };
201
+ const events = surface.wire(siteId, lineId, query);
202
+ const viewer = await (0, browser_view_1.viewerFor)(req.auth?.userId);
203
+ const body = { siteId, lineId, leading: events !== null, events: (events ?? []).map((e) => wireForViewer(e, viewer.builder)) };
204
+ res.status(200).json(body);
205
+ });
173
206
  router.post('/lines/:siteId/:lineId/inspect', auth_1.requireRoleManager, (req, res) => {
174
207
  const { surface, siteId, lineId, line } = lineOf(req);
175
208
  if (!surface || !line) {
@@ -2,15 +2,17 @@ export * from './lookup-cache';
2
2
  export * from './lookup-refs';
3
3
  export interface KnowledgeVersionMeta {
4
4
  version: number;
5
+ label: string | null;
5
6
  change_summary: string | null;
6
7
  created_at: string;
7
8
  is_current: boolean;
8
9
  }
9
- /** The immutable snapshot at (domain, key, version), uncached full shape. */
10
- export declare function getKnowledgeVersion(domain: string, key: string, version: number): Promise<{
10
+ /** The immutable snapshot at (domain, key, version), by number or by the edition's label; uncached full shape. */
11
+ export declare function getKnowledgeVersion(domain: string, key: string, version: number | string): Promise<{
11
12
  domain: string;
12
13
  key: string;
13
14
  version: number;
15
+ label: string | null;
14
16
  data: Record<string, unknown>;
15
17
  tags: string[];
16
18
  created_at: string;
@@ -22,19 +22,24 @@ exports.listKnowledgeVersions = listKnowledgeVersions;
22
22
  * writes produce, and resolves escalation lookup refs against them.
23
23
  */
24
24
  const db_1 = require("../../lib/db");
25
+ const lookup_cache_1 = require("./lookup-cache");
25
26
  const sql_1 = require("./sql");
26
27
  __exportStar(require("./lookup-cache"), exports);
27
28
  __exportStar(require("./lookup-refs"), exports);
28
- /** The immutable snapshot at (domain, key, version), uncached full shape. */
29
+ /** The immutable snapshot at (domain, key, version), by number or by the edition's label; uncached full shape. */
29
30
  async function getKnowledgeVersion(domain, key, version) {
30
31
  const pool = (0, db_1.getPool)();
31
- const { rows } = await pool.query(sql_1.GET_KNOWLEDGE_VERSION, [domain, key, version]);
32
+ const number = typeof version === 'string' ? await (0, lookup_cache_1.editionOfLabel)(domain, key, version) : version;
33
+ if (number === null)
34
+ return null;
35
+ const { rows } = await pool.query(sql_1.GET_KNOWLEDGE_VERSION, [domain, key, number]);
32
36
  if (!rows[0])
33
37
  return null;
34
38
  return {
35
39
  domain: rows[0].domain,
36
40
  key: rows[0].key,
37
41
  version: rows[0].version,
42
+ label: rows[0].label ?? null,
38
43
  data: rows[0].data ?? {},
39
44
  tags: rows[0].tags ?? [],
40
45
  created_at: rows[0].created_at.toISOString(),
@@ -46,6 +51,7 @@ async function listKnowledgeVersions(domain, key) {
46
51
  const { rows } = await pool.query(sql_1.LIST_KNOWLEDGE_VERSIONS, [domain, key]);
47
52
  return rows.map((r) => ({
48
53
  version: r.version,
54
+ label: r.label ?? null,
49
55
  change_summary: r.change_summary ?? null,
50
56
  created_at: r.created_at.toISOString(),
51
57
  is_current: r.is_current === true,
@@ -10,6 +10,8 @@ export interface ResolvedLookup {
10
10
  version: number | null;
11
11
  /** The ref follows the newest edition rather than pinning one. */
12
12
  current?: true;
13
+ /** The ref pins an edition by this label. */
14
+ label?: string;
13
15
  as?: string;
14
16
  data: Record<string, unknown> | null;
15
17
  missing?: boolean;
@@ -25,6 +27,12 @@ export declare function resolveRefEdition(ref: EscalationLookupRef, asOf?: strin
25
27
  version: number;
26
28
  snapshot: KnowledgeSnapshot;
27
29
  } | null>;
30
+ /**
31
+ * The edition a label names, or, for a label that is a whole number no edition
32
+ * carries, that edition's number. Null when the label names nothing; that is
33
+ * never cached, so a label given later becomes visible.
34
+ */
35
+ export declare function editionOfLabel(domain: string, key: string, label: string): Promise<number | null>;
28
36
  /**
29
37
  * Resolve a metadata `lookups` array into the form-context `lookup` domain:
30
38
  * { [as ?? key]: data }. Returns null when there is nothing to resolve, so
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getKnowledgeSnapshot = getKnowledgeSnapshot;
4
4
  exports.resolveRefEdition = resolveRefEdition;
5
+ exports.editionOfLabel = editionOfLabel;
5
6
  exports.resolveLookupContext = resolveLookupContext;
6
7
  exports.resolveLookupRefs = resolveLookupRefs;
7
8
  /**
@@ -18,6 +19,8 @@ const sql_1 = require("./sql");
18
19
  const SNAPSHOT_CACHE_MAX_ENTRIES = 256;
19
20
  // Map preserves insertion order — delete+set on read keeps it LRU.
20
21
  const snapshotCache = new Map();
22
+ // A label names one edition for good, so the number it resolves to is kept too.
23
+ const labelCache = new Map();
21
24
  function remember(cacheKey, snapshot) {
22
25
  snapshotCache.set(cacheKey, snapshot);
23
26
  while (snapshotCache.size > SNAPSHOT_CACHE_MAX_ENTRIES) {
@@ -52,7 +55,9 @@ async function getKnowledgeSnapshot(domain, key, version) {
52
55
  */
53
56
  async function resolveRefEdition(ref, asOf) {
54
57
  if (!(0, escalation_1.isCurrentLookupRef)(ref)) {
55
- const version = ref.version;
58
+ const version = (0, escalation_1.isLabelLookupRef)(ref) ? await editionOfLabel(ref.domain, ref.key, ref.version) : ref.version;
59
+ if (version === null)
60
+ return null;
56
61
  const snapshot = await getKnowledgeSnapshot(ref.domain, ref.key, version);
57
62
  return snapshot ? { version, snapshot } : null;
58
63
  }
@@ -71,6 +76,26 @@ async function resolveRefEdition(ref, asOf) {
71
76
  remember(cacheKey, snapshot);
72
77
  return { version, snapshot };
73
78
  }
79
+ /**
80
+ * The edition a label names, or, for a label that is a whole number no edition
81
+ * carries, that edition's number. Null when the label names nothing; that is
82
+ * never cached, so a label given later becomes visible.
83
+ */
84
+ async function editionOfLabel(domain, key, label) {
85
+ const cacheKey = `${domain} ${key} #${label}`;
86
+ const hit = labelCache.get(cacheKey);
87
+ if (hit !== undefined)
88
+ return hit;
89
+ const { rows } = await (0, db_1.getPool)().query(sql_1.GET_KNOWLEDGE_VERSION_BY_LABEL, [domain, key, label]);
90
+ if (rows[0]) {
91
+ const version = Number(rows[0].version);
92
+ labelCache.set(cacheKey, version);
93
+ while (labelCache.size > SNAPSHOT_CACHE_MAX_ENTRIES)
94
+ labelCache.delete(labelCache.keys().next().value);
95
+ return version;
96
+ }
97
+ return /^[1-9][0-9]*$/.test(label) ? Number(label) : null;
98
+ }
74
99
  /** Structural check for one ref — malformed entries in stored metadata are skipped, not fatal. */
75
100
  function isLookupRef(ref) {
76
101
  if (!ref || typeof ref !== 'object')
@@ -78,7 +103,7 @@ function isLookupRef(ref) {
78
103
  const r = ref;
79
104
  return typeof r.domain === 'string' && r.domain.length > 0
80
105
  && typeof r.key === 'string' && r.key.length > 0
81
- && ((0, escalation_1.isCurrentLookupRef)(r) || (typeof r.version === 'number' && Number.isInteger(r.version) && r.version >= 1))
106
+ && ((0, escalation_1.isCurrentLookupRef)(r) || (0, escalation_1.isLabelLookupRef)(r) || (typeof r.version === 'number' && Number.isInteger(r.version) && r.version >= 1))
82
107
  && (r.as === undefined || (typeof r.as === 'string' && r.as.length > 0));
83
108
  }
84
109
  /**
@@ -112,12 +137,14 @@ async function resolveLookupRefs(refs, asOf) {
112
137
  if (!isLookupRef(ref))
113
138
  continue;
114
139
  const current = (0, escalation_1.isCurrentLookupRef)(ref);
140
+ const label = (0, escalation_1.isLabelLookupRef)(ref) ? ref.version : undefined;
115
141
  const edition = await resolveRefEdition(ref, asOf);
116
142
  resolved.push({
117
143
  domain: ref.domain,
118
144
  key: ref.key,
119
- version: edition?.version ?? (current ? null : ref.version),
145
+ version: edition?.version ?? (current || label !== undefined ? null : ref.version),
120
146
  ...(current ? { current: true } : {}),
147
+ ...(label !== undefined ? { label } : {}),
121
148
  ...(ref.as ? { as: ref.as } : {}),
122
149
  data: edition ? edition.snapshot.data : null,
123
150
  ...(edition ? {} : { missing: true }),
@@ -21,7 +21,7 @@ async function describeMissingLookupRefs(refs) {
21
21
  const where = versions.length
22
22
  ? `editions: ${versions.map((v) => `v${v}`).join(', ')}`
23
23
  : `no knowledge entry ${ref.domain}/${ref.key}`;
24
- const named = (0, escalation_1.isCurrentLookupRef)(ref) ? 'current' : `v${ref.version}`;
24
+ const named = (0, escalation_1.isCurrentLookupRef)(ref) ? 'current' : (0, escalation_1.isLabelLookupRef)(ref) ? `label ${JSON.stringify(ref.version)}` : `v${ref.version}`;
25
25
  problems.push(`Lookup ref ${ref.domain}/${ref.key} ${named} names no edition (${where})`);
26
26
  }
27
27
  return problems;
@@ -1,4 +1,6 @@
1
- export declare const GET_KNOWLEDGE_VERSION = "\n SELECT domain, key, version, data, tags, change_summary, created_at\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2 AND version = $3";
2
- export declare const LIST_KNOWLEDGE_VERSIONS = "\n SELECT v.version, v.change_summary, v.created_at,\n (v.version = k.current_version) AS is_current\n FROM lt_knowledge_versions v\n JOIN lt_knowledge k ON k.domain = v.domain AND k.key = v.key\n WHERE v.domain = $1 AND v.key = $2\n ORDER BY v.version DESC";
1
+ export declare const GET_KNOWLEDGE_VERSION = "\n SELECT domain, key, version, label, data, tags, change_summary, created_at\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2 AND version = $3";
2
+ /** The edition a label names, when one does. */
3
+ export declare const GET_KNOWLEDGE_VERSION_BY_LABEL = "\n SELECT version\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2 AND label = $3";
4
+ export declare const LIST_KNOWLEDGE_VERSIONS = "\n SELECT v.version, v.label, v.change_summary, v.created_at,\n (v.version = k.current_version) AS is_current\n FROM lt_knowledge_versions v\n JOIN lt_knowledge k ON k.domain = v.domain AND k.key = v.key\n WHERE v.domain = $1 AND v.key = $2\n ORDER BY v.version DESC";
3
5
  /** The newest edition, or the newest created at or before $3 when it is set. */
4
6
  export declare const GET_LATEST_KNOWLEDGE_VERSION = "\n SELECT version, data, tags\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2\n AND ($3::timestamptz IS NULL OR created_at <= $3)\n ORDER BY version DESC\n LIMIT 1";
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GET_LATEST_KNOWLEDGE_VERSION = exports.LIST_KNOWLEDGE_VERSIONS = exports.GET_KNOWLEDGE_VERSION = void 0;
3
+ exports.GET_LATEST_KNOWLEDGE_VERSION = exports.LIST_KNOWLEDGE_VERSIONS = exports.GET_KNOWLEDGE_VERSION_BY_LABEL = exports.GET_KNOWLEDGE_VERSION = void 0;
4
4
  exports.GET_KNOWLEDGE_VERSION = `
5
- SELECT domain, key, version, data, tags, change_summary, created_at
5
+ SELECT domain, key, version, label, data, tags, change_summary, created_at
6
6
  FROM lt_knowledge_versions
7
7
  WHERE domain = $1 AND key = $2 AND version = $3`;
8
+ /** The edition a label names, when one does. */
9
+ exports.GET_KNOWLEDGE_VERSION_BY_LABEL = `
10
+ SELECT version
11
+ FROM lt_knowledge_versions
12
+ WHERE domain = $1 AND key = $2 AND label = $3`;
8
13
  exports.LIST_KNOWLEDGE_VERSIONS = `
9
- SELECT v.version, v.change_summary, v.created_at,
14
+ SELECT v.version, v.label, v.change_summary, v.created_at,
10
15
  (v.version = k.current_version) AS is_current
11
16
  FROM lt_knowledge_versions v
12
17
  JOIN lt_knowledge k ON k.domain = v.domain AND k.key = v.key
@@ -42,6 +42,8 @@ export interface MachineLineConfig {
42
42
  progressStep: number;
43
43
  /** Telemetry messages are published as wire events too; off by default, since they are read live and many. */
44
44
  wireTelemetry: boolean;
45
+ /** How many of the line's last wire events the leader keeps for GET .../wire. */
46
+ wireWindow: number;
45
47
  ltBaseUrl: string;
46
48
  }
47
49
  type Env = Record<string, string | undefined>;
@@ -75,6 +75,7 @@ function loadMachineLineConfig(env = process.env) {
75
75
  lifecycle: oneOf('MACHINE_LIFECYCLE', env.MACHINE_LIFECYCLE || SWITCH.OFF, Object.values(SWITCH)) === SWITCH.ON,
76
76
  progressStep: percentStep(env, 'MACHINE_PROGRESS_STEP', 25),
77
77
  wireTelemetry: oneOf('MACHINE_WIRE_TELEMETRY', env.MACHINE_WIRE_TELEMETRY || SWITCH.OFF, Object.values(SWITCH)) === SWITCH.ON,
78
+ wireWindow: Math.max(1, int(env, 'MACHINE_WIRE_WINDOW', 500)),
78
79
  ltBaseUrl: env.LT_BASE_URL ?? 'http://localhost:3000',
79
80
  };
80
81
  if (transport === exports.MACHINE_TRANSPORT.NATS) {
@@ -16,12 +16,16 @@ export declare const ROW_TYPE: {
16
16
  readonly CONTRACT_VIOLATION: "contract-violation";
17
17
  /** An address claims a station the line already knows by another address, or the reverse: a person approves or rejects. */
18
18
  readonly MACHINE_REGISTRATION: "machine-registration";
19
+ /** Units a machine finished on hold wait for a quality decision: release, rework or scrap. */
20
+ readonly QUALITY_HOLD: "quality-hold";
19
21
  };
20
22
  export declare const LINE_ROLE: {
21
23
  /** People who clear alarms, bring machines back and decide failed handoffs. */
22
24
  readonly OPERATOR: "line-operator";
23
25
  /** People who take finished units off the line: the final handoff. */
24
26
  readonly EXIT: "line-exit";
27
+ /** People who decide units a machine finished on hold. */
28
+ readonly QUALITY: "line-quality";
25
29
  /** The gateway's own identity. */
26
30
  readonly CONTROLLER: "line-controller";
27
31
  };
@@ -116,6 +120,8 @@ export declare const HANDOFF_ERROR: {
116
120
  readonly SOFTWARE_UNSUPPORTED: "software-unsupported";
117
121
  readonly WITHDRAWN: "withdrawn";
118
122
  readonly CANCELLED: "cancelled";
123
+ /** An arrival no open handoff into the machine expects, shown to the operator role once per machine and carrier. */
124
+ readonly UNMATCHED_ARRIVAL: "unmatched-arrival";
119
125
  };
120
126
  export type HandoffErrorCode = typeof HANDOFF_ERROR[keyof typeof HANDOFF_ERROR];
121
127
  /** A material lot a machine consumed for a unit. */
@@ -172,6 +178,10 @@ export declare const DECISION: {
172
178
  readonly RESTORED: "restored";
173
179
  readonly APPROVE: "approve";
174
180
  readonly REJECT: "reject";
181
+ /** A quality decision on held units: they continue from where they are. */
182
+ readonly RELEASE: "release";
183
+ /** A quality decision on held units: they run again from a station on the route. */
184
+ readonly REWORK: "rework";
175
185
  };
176
186
  export type Decision = typeof DECISION[keyof typeof DECISION];
177
187
  /** The decisions an operator may take on a failed handoff; retry only while every unit is still at the sender. */
@@ -180,11 +190,19 @@ export interface OperatorResolution {
180
190
  decision: Decision;
181
191
  note?: string;
182
192
  }
193
+ /** The decisions a quality role may take on units a machine finished on hold. */
194
+ export declare const HOLD_DECISIONS: readonly Decision[];
195
+ export interface QualityResolution extends OperatorResolution {
196
+ /** For `rework`: the station the units run again from; the holding station when absent. */
197
+ station?: string;
198
+ }
183
199
  /** Signal keys for the unit workflow; every attempt mints a fresh key. */
184
200
  export declare const signalKeys: {
185
201
  handoff: (jobKey: string, step: number, attempt: number) => string;
186
202
  handoffError: (jobKey: string, step: number, attempt: number, round?: number) => string;
187
203
  jobStop: (jobKey: string, step: number, attempt: number) => string;
204
+ hold: (jobKey: string, step: number, attempt: number, round?: number) => string;
205
+ disposition: (jobKey: string, step: number, attempt: number) => string;
188
206
  };
189
207
  /** Metadata on a machine-command row. */
190
208
  export interface CommandFacets {
@@ -6,7 +6,7 @@
6
6
  * protocol that resolves it differs.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.cancelCommandId = exports.controlCommandId = exports.dispatchCommandId = exports.COMMAND_STATUS = exports.signalKeys = exports.DECISION = exports.LIFECYCLE_FACET = exports.TRACE_FACET = exports.HANDOFF_ERROR = exports.itemKeys = exports.LINE_ROLE = exports.ROW_TYPE = void 0;
9
+ exports.cancelCommandId = exports.controlCommandId = exports.dispatchCommandId = exports.COMMAND_STATUS = exports.signalKeys = exports.HOLD_DECISIONS = exports.DECISION = exports.LIFECYCLE_FACET = exports.TRACE_FACET = exports.HANDOFF_ERROR = exports.itemKeys = exports.LINE_ROLE = exports.ROW_TYPE = void 0;
10
10
  exports.arrivedUnit = arrivedUnit;
11
11
  exports.handoffProgress = handoffProgress;
12
12
  exports.handoffDecisions = handoffDecisions;
@@ -24,12 +24,16 @@ exports.ROW_TYPE = {
24
24
  CONTRACT_VIOLATION: 'contract-violation',
25
25
  /** An address claims a station the line already knows by another address, or the reverse: a person approves or rejects. */
26
26
  MACHINE_REGISTRATION: 'machine-registration',
27
+ /** Units a machine finished on hold wait for a quality decision: release, rework or scrap. */
28
+ QUALITY_HOLD: 'quality-hold',
27
29
  };
28
30
  exports.LINE_ROLE = {
29
31
  /** People who clear alarms, bring machines back and decide failed handoffs. */
30
32
  OPERATOR: line_1.DEFAULT_OPERATOR_ROLE,
31
33
  /** People who take finished units off the line: the final handoff. */
32
34
  EXIT: line_1.DEFAULT_EXIT_ROLE,
35
+ /** People who decide units a machine finished on hold. */
36
+ QUALITY: line_1.DEFAULT_QUALITY_ROLE,
33
37
  /** The gateway's own identity. */
34
38
  CONTROLLER: 'line-controller',
35
39
  };
@@ -75,6 +79,8 @@ exports.HANDOFF_ERROR = {
75
79
  SOFTWARE_UNSUPPORTED: 'software-unsupported',
76
80
  WITHDRAWN: 'withdrawn',
77
81
  CANCELLED: 'cancelled',
82
+ /** An arrival no open handoff into the machine expects, shown to the operator role once per machine and carrier. */
83
+ UNMATCHED_ARRIVAL: 'unmatched-arrival',
78
84
  ...protocol_1.TRANSFER_FAILURE,
79
85
  };
80
86
  /**
@@ -95,6 +101,10 @@ exports.DECISION = {
95
101
  RESTORED: 'restored',
96
102
  APPROVE: 'approve',
97
103
  REJECT: 'reject',
104
+ /** A quality decision on held units: they continue from where they are. */
105
+ RELEASE: 'release',
106
+ /** A quality decision on held units: they run again from a station on the route. */
107
+ REWORK: 'rework',
98
108
  };
99
109
  /**
100
110
  * Failures a retry cannot repair. Units a sender finished as not ok leave the
@@ -107,11 +117,15 @@ function handoffDecisions(errorCode) {
107
117
  const noRetry = !!errorCode && NO_RETRY.includes(errorCode);
108
118
  return noRetry ? [exports.DECISION.MANUAL, exports.DECISION.SCRAP] : [exports.DECISION.RETRY, exports.DECISION.MANUAL, exports.DECISION.SCRAP];
109
119
  }
120
+ /** The decisions a quality role may take on units a machine finished on hold. */
121
+ exports.HOLD_DECISIONS = [exports.DECISION.RELEASE, exports.DECISION.REWORK, exports.DECISION.SCRAP];
110
122
  /** Signal keys for the unit workflow; every attempt mints a fresh key. */
111
123
  exports.signalKeys = {
112
124
  handoff: (jobKey, step, attempt) => `handoff-${jobKey}-s${step}-a${attempt}`,
113
125
  handoffError: (jobKey, step, attempt, round = 1) => `handoff-error-${jobKey}-s${step}-a${attempt}${round > 1 ? `-r${round}` : ''}`,
114
126
  jobStop: (jobKey, step, attempt) => `job-stop-${jobKey}-s${step}-a${attempt}`,
127
+ hold: (jobKey, step, attempt, round = 1) => `hold-${jobKey}-s${step}-a${attempt}${round > 1 ? `-r${round}` : ''}`,
128
+ disposition: (jobKey, step, attempt) => `disposition-${jobKey}-s${step}-a${attempt}`,
115
129
  };
116
130
  exports.COMMAND_STATUS = { ...protocol_1.REPLY_STATUS, NO_REPLY: 'no-reply', UNSUPPORTED: 'unsupported' };
117
131
  /** A row's dispatch command in its validity round; a later round is a new command (gateway/pacing.ts dispatchWindow). */
@@ -36,7 +36,7 @@ export declare const OPERATOR_FORM_SCHEMA: {
36
36
  properties: {
37
37
  decision: {
38
38
  type: string;
39
- enum: ("reject" | "scrap" | "retry" | "manual" | "acknowledge" | "cleared" | "restored" | "approve")[];
39
+ enum: ("release" | "reject" | "scrap" | "rework" | "retry" | "manual" | "acknowledge" | "cleared" | "restored" | "approve")[];
40
40
  default: "acknowledge";
41
41
  description: string;
42
42
  };
@@ -53,3 +53,28 @@ export declare const OPERATOR_FORM_SCHEMA: {
53
53
  };
54
54
  };
55
55
  };
56
+ export declare const QUALITY_FORM_SCHEMA: {
57
+ title: string;
58
+ description: string;
59
+ 'x-lt-order': string[];
60
+ required: string[];
61
+ properties: {
62
+ decision: {
63
+ type: string;
64
+ enum: ("release" | "scrap" | "rework")[];
65
+ default: "release";
66
+ description: string;
67
+ };
68
+ station: {
69
+ type: string;
70
+ default: string;
71
+ description: string;
72
+ };
73
+ note: {
74
+ type: string;
75
+ format: string;
76
+ default: string;
77
+ description: string;
78
+ };
79
+ };
80
+ };
@@ -6,7 +6,7 @@
6
6
  * handoffs and close alarm and machine-state rows.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.OPERATOR_FORM_SCHEMA = exports.HANDOFF_FORM_SCHEMA = void 0;
9
+ exports.QUALITY_FORM_SCHEMA = exports.OPERATOR_FORM_SCHEMA = exports.HANDOFF_FORM_SCHEMA = void 0;
10
10
  const contracts_1 = require("./contracts");
11
11
  exports.HANDOFF_FORM_SCHEMA = {
12
12
  title: 'Receive units',
@@ -35,3 +35,19 @@ exports.OPERATOR_FORM_SCHEMA = {
35
35
  clearedBy: { type: 'string', default: '', description: 'Who cleared it at the machine, when the machine reports it' },
36
36
  },
37
37
  };
38
+ exports.QUALITY_FORM_SCHEMA = {
39
+ title: 'Quality decision',
40
+ description: 'Units a machine finished on hold: release them to continue, send them back to a station for rework, or scrap them.',
41
+ 'x-lt-order': ['decision', 'station', 'note'],
42
+ required: ['decision'],
43
+ properties: {
44
+ decision: {
45
+ type: 'string',
46
+ enum: [contracts_1.DECISION.RELEASE, contracts_1.DECISION.REWORK, contracts_1.DECISION.SCRAP],
47
+ default: contracts_1.DECISION.RELEASE,
48
+ description: 'Release, rework or scrap',
49
+ },
50
+ station: { type: 'string', default: '', description: 'For rework: the station the units run again from; the holding station when empty' },
51
+ note: { type: 'string', format: 'textarea', default: '', description: 'What was found' },
52
+ },
53
+ };
@@ -18,14 +18,19 @@ export interface CreateRowInput {
18
18
  escalation_payload?: string;
19
19
  }
20
20
  export type Action =
21
- /** `receivedOnAccept`: the units are already at the machine, so its acceptance completes the handoff. */
21
+ /**
22
+ * `receivedOnAccept`: the units are already at the machine, so its acceptance completes the handoff.
23
+ * `ended`: the row already resolved; the job is sent again for units the machine still holds, and no verb touches the row.
24
+ */
22
25
  {
23
26
  kind: 'dispatch';
24
27
  rowId: string;
25
28
  machineId: string;
26
29
  cmdId: string;
27
30
  args: Record<string, any>;
31
+ issuedAt?: number;
28
32
  receivedOnAccept?: boolean;
33
+ ended?: boolean;
29
34
  } | {
30
35
  kind: 'command';
31
36
  machineId: string;
@@ -79,6 +84,7 @@ export type Action =
79
84
  status: string;
80
85
  reason: string;
81
86
  code?: string;
87
+ preparing?: boolean;
82
88
  }
83
89
  /** A machine reported the jobs it holds: answers a status query in flight. */
84
90
  | {
@@ -99,6 +105,12 @@ export type Action =
99
105
  machineId: string;
100
106
  address: string;
101
107
  reason: string;
108
+ }
109
+ /** Jobs a restarted machine lost that this gateway cannot send again: nothing of them is remembered. */
110
+ | {
111
+ kind: 'unrecoverable';
112
+ machineId: string;
113
+ jobs: string[];
102
114
  };
103
115
  /** Once the row holds every one of `keys`: resolve it with `payload`, or run `then` instead. */
104
116
  export interface Completion {
@@ -4,8 +4,9 @@
4
4
  * and under a strict codec a message that breaks the contract is held for a
5
5
  * person instead of acted on.
6
6
  */
7
+ import type { OpenRowQuery } from '../rows';
7
8
  import { type LineDef } from '../../../lib/machines/line';
8
- import type { Inspection } from '../../../lib/machines/protocol';
9
+ import { type Inspection } from '../../../lib/machines/protocol';
9
10
  import { type Disposition, type InboundMessage } from '../../../lib/machines/transport';
10
11
  import type { WireAction } from '../../../types/machines-view';
11
12
  import type { Action } from './actions';
@@ -36,6 +37,8 @@ export interface AdmissionContext {
36
37
  log: GatewayLog;
37
38
  /** Send waiting handoffs that may now go out. */
38
39
  pace(): Promise<void>;
40
+ /** The controller's pending rows of a type, for the rows shown to people once. */
41
+ openRows(query: OpenRowQuery): Promise<Record<string, any>[]>;
39
42
  }
40
43
  /**
41
44
  * The station a message is from (registration.ts). A newly claimed station is
@@ -45,3 +48,14 @@ export interface AdmissionContext {
45
48
  export declare function admitSender(ctx: AdmissionContext, topic: string, raw: Inspection): Promise<Admitted | Applied>;
46
49
  /** Under a strict codec a broken message is not acted on: a person sees it on a row, once. */
47
50
  export declare function holdViolation(ctx: AdmissionContext, msg: InboundMessage, seen: Inspection): Promise<Applied>;
51
+ /** A job arrival: a scan, or a transfer that failed on the carrier read. */
52
+ export declare function isArrival(seen: Inspection): boolean;
53
+ /** An arrival that read no id at all: no row can ever claim it, so it goes to a person on its first delivery. */
54
+ export declare function isUnreadableArrival(seen: Inspection): boolean;
55
+ /**
56
+ * An arrival no open handoff into the machine expects, after every delivery:
57
+ * a person sees it on one handoff-error row per machine and carrier, with the
58
+ * raw arrival, so the unit can be re-scanned or routed. Resolving the row lets
59
+ * the next such arrival raise one again.
60
+ */
61
+ export declare function holdArrival(ctx: AdmissionContext, msg: InboundMessage, seen: Inspection): Promise<Applied>;
@@ -8,8 +8,12 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.admitSender = admitSender;
10
10
  exports.holdViolation = holdViolation;
11
+ exports.isArrival = isArrival;
12
+ exports.isUnreadableArrival = isUnreadableArrival;
13
+ exports.holdArrival = holdArrival;
11
14
  const contracts_1 = require("../contracts");
12
15
  const line_1 = require("../../../lib/machines/line");
16
+ const protocol_1 = require("../../../lib/machines/protocol");
13
17
  const transport_1 = require("../../../lib/machines/transport");
14
18
  const dispatcher_1 = require("./dispatcher");
15
19
  const log_1 = require("./log");
@@ -79,3 +83,46 @@ async function holdViolation(ctx, msg, seen) {
79
83
  return { disposition: transport_1.DISPOSITION.NAK, failed: true, actions: [] };
80
84
  }
81
85
  }
86
+ /** A job arrival: a scan, or a transfer that failed on the carrier read. */
87
+ function isArrival(seen) {
88
+ const event = seen.body?.body?.event;
89
+ return seen.parsed?.kind === protocol_1.MESSAGE_KIND.JOB && (event === protocol_1.JOB_EVENT.ARRIVED || event === protocol_1.JOB_EVENT.TRANSFER_FAILED);
90
+ }
91
+ /** An arrival that read no id at all: no row can ever claim it, so it goes to a person on its first delivery. */
92
+ function isUnreadableArrival(seen) {
93
+ return seen.parsed?.kind === protocol_1.MESSAGE_KIND.JOB && (0, protocol_1.isUnreadableRead)(seen.body?.body ?? {});
94
+ }
95
+ /**
96
+ * An arrival no open handoff into the machine expects, after every delivery:
97
+ * a person sees it on one handoff-error row per machine and carrier, with the
98
+ * raw arrival, so the unit can be re-scanned or routed. Resolving the row lets
99
+ * the next such arrival raise one again.
100
+ */
101
+ async function holdArrival(ctx, msg, seen) {
102
+ const { ident } = seen.parsed;
103
+ const body = seen.body?.body ?? {};
104
+ const carrierId = typeof body.carrierId === 'string' ? body.carrierId : null;
105
+ const unitId = typeof body.unitId === 'string' ? body.unitId : null;
106
+ const detail = `no handoff into ${ident.machineId} expects ${carrierId ? `carrier ${carrierId}` : 'an unreadable carrier'}`;
107
+ try {
108
+ const open = await ctx.openRows({ type: contracts_1.ROW_TYPE.HANDOFF_ERROR, role: (0, line_1.operatorRole)(ctx.line) });
109
+ const shown = open.find((r) => r.metadata?.errorCode === contracts_1.HANDOFF_ERROR.UNMATCHED_ARRIVAL && r.metadata?.machineId === ident.machineId && (r.metadata?.carrierId ?? null) === carrierId);
110
+ if (shown)
111
+ return { disposition: transport_1.DISPOSITION.ACK, failed: false, actions: [{ verb: 'unmatched', rowId: shown.id, detail: `${detail} (already shown to a person)` }] };
112
+ const [made] = await ctx.executor.apply([{ kind: 'createRow', row: {
113
+ type: contracts_1.ROW_TYPE.HANDOFF_ERROR, role: (0, line_1.operatorRole)(ctx.line), priority: 2,
114
+ description: `${ident.machineId} reports an arrival no handoff expects: ${carrierId ? `carrier ${carrierId}` : 'the carrier could not be read'}`,
115
+ metadata: {
116
+ siteId: ident.siteId, lineId: ident.lineId, machineId: ident.machineId, errorCode: contracts_1.HANDOFF_ERROR.UNMATCHED_ARRIVAL,
117
+ carrierId, unitId, event: body.event, topic: msg.topic, messageId: seen.body?.id ?? null, arrival: body,
118
+ },
119
+ escalation_payload: msg.payload,
120
+ } }]);
121
+ return { disposition: transport_1.DISPOSITION.ACK, failed: false, actions: [{ verb: 'create-row', rowId: made?.rowId, detail }] };
122
+ }
123
+ catch (err) {
124
+ if (!(err instanceof dispatcher_1.GatewayStopped))
125
+ ctx.log.error(log_1.GATEWAY_LOG.VERB_FAILED, { topic: msg.topic, error: err.message });
126
+ return { disposition: transport_1.DISPOSITION.NAK, failed: true, actions: [] };
127
+ }
128
+ }