@semiont/jobs 0.5.39 → 0.6.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.
package/README.md CHANGED
@@ -48,10 +48,7 @@ const job: PendingJob<DetectionParams> = {
48
48
  metadata: {
49
49
  id: jobId('job-abc123'),
50
50
  type: 'reference-annotation',
51
- userId: userId('user@example.com'),
52
- userName: 'Jane Doe',
53
- userEmail: 'jane@example.com',
54
- userDomain: 'example.com',
51
+ userId: userId('did:web:example.com:users:f47ac10b-58cc-4372-a567-0e02b2c3d479'),
55
52
  created: new Date().toISOString(),
56
53
  retryCount: 0,
57
54
  maxRetries: 1,
@@ -91,10 +88,7 @@ All jobs share common metadata:
91
88
  interface JobMetadata {
92
89
  id: JobId;
93
90
  type: JobType;
94
- userId: UserId;
95
- userName: string; // Audit-only snapshot of the requesting user
96
- userEmail: string; // Audit-only snapshot of the requesting user
97
- userDomain: string; // Audit-only snapshot of the requesting user
91
+ userId: UserId; // Who requested it — the verified DID, the job's only identity
98
92
  created: string;
99
93
  retryCount: number;
100
94
  maxRetries: number;
@@ -103,7 +97,7 @@ interface JobMetadata {
103
97
  }
104
98
  ```
105
99
 
106
- The `userName`, `userEmail`, and `userDomain` fields are an audit-only snapshot of the requesting user, persisted in the on-disk job file. Workers derive annotation `creator` attribution from `userId` via `didToAgent()`. `completedUnits` is written only by `failJob`, unioned across attempts, and carried on the `job:fail` event — see Failure discipline below.
100
+ `userId` is the job's only identity — the DID the gateway verified on the `job:create`. The dispatcher records it as the requester when it accepts a claim, and that record is what lets the knowledge base attribute a write citing this job; a worker never states it. `completedUnits` is written only by `failJob`, unioned across attempts, and carried on the `job:fail` event — see Failure discipline below.
107
101
 
108
102
  ## Annotation Workers
109
103
 
@@ -142,7 +136,7 @@ Workers are not subclassed. To add a job type:
142
136
  2. Add a `process*Job` function in `src/processors.ts` that runs the inference and returns the annotations/result.
143
137
  3. Dispatch the new `jobType` to that processor in `handleJobInner()` in `src/worker-process.ts`.
144
138
 
145
- Processors are transport-agnostic: they take content, an `InferenceClient`, the job params, the user id, the `generator` (W3C SoftwareAgent), and an `onProgress` callback, and return annotations plus a result. The worker process handles claiming, content fetching, and lifecycle event emission.
139
+ Processors are transport-agnostic: they take content, an `InferenceClient`, the job params, a `buildAnnotation` closure (which carries the `generator` — the worker's own `Software` agent), an `onProgress` callback and a per-chunk commit callback, and return a result. No user identity reaches a processor: an annotation states what produced it, and who requested it is derived by the knowledge base from the job the commit cites. The worker process handles claiming, content fetching, committing, and lifecycle event emission.
146
140
 
147
141
  ## Discriminated Unions
148
142
 
package/dist/index.d.ts CHANGED
@@ -23,17 +23,14 @@ type JobStatus = 'pending' | 'running' | 'complete' | 'failed' | 'cancelled';
23
23
  interface JobMetadata {
24
24
  id: JobId;
25
25
  type: JobType;
26
- userId: UserId;
27
26
  /**
28
- * Audit-only snapshot of the requesting user (with `userEmail` and
29
- * `userDomain` below), stamped at job creation and persisted in the
30
- * on-disk job file. No code path reads these back — annotation
31
- * `creator` attribution is derived from `userId` via `didToAgent()`.
32
- * Kept intentionally so job files are self-describing to a human.
27
+ * Who requested the job: the verified DID the gateway stamped on the
28
+ * `job:create`, and the ONLY identity a job carries. The dispatcher
29
+ * records it as the requester on `job:assigned`, which is what lets a
30
+ * write citing this job be attributed — so nothing else about the
31
+ * requester needs to travel with the job, or be trusted from it.
33
32
  */
34
- userName: string;
35
- userEmail: string;
36
- userDomain: string;
33
+ userId: UserId;
37
34
  created: string;
38
35
  retryCount: number;
39
36
  maxRetries: number;
@@ -524,7 +521,9 @@ declare class FsJobQueue implements JobQueue {
524
521
 
525
522
  /**
526
523
  * JetStreamJobQueue — the JetStream driver behind the `JobQueue` interface
527
- * (JOB-QUEUE-DRIVER P1, topology ruling M: gateway-mediated).
524
+ * (JOB-QUEUE-DRIVER P1, topology ruling M: broker-mediated — the queue's own
525
+ * process holds the broker connection and the leases; workers never touch the
526
+ * broker. That process was the gateway; it is the dispatcher now.)
528
527
  *
529
528
  * Two primitives, one authority each:
530
529
  *
@@ -535,12 +534,12 @@ declare class FsJobQueue implements JobQueue {
535
534
  * - **Stream `JOBS`** (subjects `jobs.<category>.<type>`, work-queue
536
535
  * retention) is the DELIVERY vehicle and redelivery timer. A delivered
537
536
  * message is the lease this process holds for a job; `working()`
538
- * heartbeats extend it, and a gateway that dies stops heartbeating, so
539
- * `AckWait` redelivers the job to a live instance — gateway-death
537
+ * heartbeats extend it, and a dispatcher that dies stops heartbeating, so
538
+ * `AckWait` redelivers the job to a live instance — dispatcher-death
540
539
  * recovery, protocol-native.
541
540
  *
542
541
  * Worker-death recovery is NOT AckWait's job under the mediated topology
543
- * (the gateway holding the lease is alive; the worker vanished): it is the
542
+ * (the dispatcher holding the lease is alive; the worker vanished): it is the
544
543
  * `lastProgressAt` sweep (`recoverStaleRunningJobs`), the same contract the
545
544
  * fs driver's mtime janitor implemented. The redelivery handler checks KV
546
545
  * state, so the two recovery paths can never double-apply.
@@ -566,6 +565,14 @@ declare const JOBS_STREAM_SUBJECTS: readonly ["jobs.>"];
566
565
  interface JetStreamJobQueueOptions {
567
566
  /** NATS server address(es), e.g. "192.168.64.42:4222". */
568
567
  servers: string | string[];
568
+ /**
569
+ * Broker credentials (INTER-COMPONENT-ACCESS P3). The same broker the signal
570
+ * plane connects to, so the same pair. Absent means an unauthenticated
571
+ * broker — which is what every deployment had before this, and means anyone
572
+ * who can reach NATS can read and write the job stream.
573
+ */
574
+ user?: string;
575
+ pass?: string;
569
576
  /** Worker presumed dead after this long without progress (default 30 min). */
570
577
  staleRunningMs?: number;
571
578
  /** Lease redelivery window when THIS process stops heartbeating (default 30 s). */
@@ -597,13 +604,15 @@ declare class JetStreamJobQueue implements JobQueue {
597
604
  private readonly tickMs;
598
605
  constructor(options: JetStreamJobQueueOptions, logger: Logger, eventBus?: EventBus | undefined);
599
606
  initialize(): Promise<void>;
607
+ /** An outage is never silent: one line down, one line back, one line if the client gives up. */
608
+ private watchConnection;
600
609
  destroy(): void;
601
610
  /**
602
- * A delivery is a job arriving at this gateway. What it means depends on
611
+ * A delivery is a job arriving at this dispatcher. What it means depends on
603
612
  * the job's authoritative (KV) state:
604
613
  * - pending → hold the lease, announce for a worker to claim;
605
614
  * - running → hold silently (a claim raced ahead of the delivery, or a
606
- * redelivery reached a fresh instance after a gateway death);
615
+ * redelivery reached a fresh instance after a dispatcher death);
607
616
  * - terminal → the work already concluded elsewhere; consume the message;
608
617
  * - unknown → not ours to run; terminate it.
609
618
  */
@@ -1019,12 +1028,13 @@ declare function generateResourceFromTopic(topic: string, entityTypes: string[],
1019
1028
 
1020
1029
  /**
1021
1030
  * Stall watchdog (WORKER-LIVENESS.md P3) — the fail-fast line behind the
1022
- * inference timeout. There is no poll loop to heartbeat; the honest
1023
- * stall signal in this push-driven architecture is *processing without
1024
- * activity*: an agent holding a claimed job whose `lastActivityAt`
1025
- * (claim / progress / finish) has stopped advancing is wedged — the
1026
- * adapter ignores every announcement while `isProcessing`, so a wedged
1027
- * agent never recovers on its own. Silent hang → loud crash → whatever
1031
+ * inference timeout. There is no poll timer to heartbeat — the worker pulls
1032
+ * when idle, and a parked idle worker is not a stall; the honest stall
1033
+ * signal is *processing without activity*: an agent holding a claimed job
1034
+ * whose `lastActivityAt` (claim / progress / finish) has stopped advancing
1035
+ * is wedged — the adapter defers every wake-up while a job is held and
1036
+ * pulls at settle, so a wedged agent never settles and never recovers on
1037
+ * its own. Silent hang → loud crash → whatever
1028
1038
  * restart policy the deployment chose.
1029
1039
  *
1030
1040
  * Thresholds are fixed by design (no env knobs) and deliberately
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { promises, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs';
2
2
  import * as path from 'path';
3
3
  import { join } from 'path';
4
4
  import { replyChannelsFor, jobId, deriveViews, GENERATABLE_MEDIA_TYPES, estimateTokens, isObject, isString, reconcileSelector, getLocaleEnglishName, cutChunk, chunkText } from '@semiont/core';
5
- import { connect, RetentionPolicy, DeliverPolicy, AckPolicy, nanos } from 'nats';
5
+ import { connect, RetentionPolicy, DeliverPolicy, AckPolicy, nanos, NatsError } from 'nats';
6
6
  import { withSpan, recordAnchorOutcome, recordDetectionCall } from '@semiont/observability';
7
7
  import { StructuredReadError } from '@semiont/inference';
8
8
  import { execFileSync } from 'child_process';
@@ -556,9 +556,18 @@ var JetStreamJobQueue = class {
556
556
  async initialize() {
557
557
  this.nc = await connect({
558
558
  servers: this.options.servers,
559
+ // The same broker as the signal plane, so the same credentials. Connect
560
+ // options rather than a URL: this address is logged on every reconnect.
561
+ ...this.options.user === void 0 ? {} : { user: this.options.user },
562
+ ...this.options.pass === void 0 ? {} : { pass: this.options.pass },
559
563
  reconnect: this.options.reconnect ?? true,
564
+ // For as long as the broker is unreachable: a manual broker restart is
565
+ // the recovery, and the library's ten attempts (~20 s) closed the
566
+ // signal plane's connection for good in its first live outage.
567
+ maxReconnectAttempts: -1,
560
568
  timeout: 1e4
561
569
  });
570
+ this.watchConnection();
562
571
  this.js = this.nc.jetstream();
563
572
  this.jsm = await this.nc.jetstreamManager();
564
573
  this.kv = await this.js.views.kv(BUCKET);
@@ -625,6 +634,26 @@ var JetStreamJobQueue = class {
625
634
  }, this.tickMs);
626
635
  this.tick.unref?.();
627
636
  }
637
+ /** An outage is never silent: one line down, one line back, one line if the client gives up. */
638
+ watchConnection() {
639
+ const servers = this.options.servers;
640
+ void (async () => {
641
+ for await (const status of this.nc.status()) {
642
+ if (status.type === "disconnect") {
643
+ this.logger.warn("[jobs BROKER-DOWN] NATS connection lost; queue operations fail until reconnect", { servers });
644
+ } else if (status.type === "reconnect") {
645
+ this.logger.info("[jobs BROKER-RECONNECTED] NATS connection restored", { servers });
646
+ }
647
+ }
648
+ })();
649
+ void this.nc.closed().then((err) => {
650
+ if (!err) return;
651
+ this.logger.error("[jobs BROKER-CLOSED] NATS connection closed; the client will not reconnect", {
652
+ servers,
653
+ reason: err instanceof NatsError ? err.code : err.message
654
+ });
655
+ });
656
+ }
628
657
  destroy() {
629
658
  this.iter?.stop();
630
659
  this.iter = null;
@@ -636,11 +665,11 @@ var JetStreamJobQueue = class {
636
665
  void this.nc?.close();
637
666
  }
638
667
  /**
639
- * A delivery is a job arriving at this gateway. What it means depends on
668
+ * A delivery is a job arriving at this dispatcher. What it means depends on
640
669
  * the job's authoritative (KV) state:
641
670
  * - pending → hold the lease, announce for a worker to claim;
642
671
  * - running → hold silently (a claim raced ahead of the delivery, or a
643
- * redelivery reached a fresh instance after a gateway death);
672
+ * redelivery reached a fresh instance after a dispatcher death);
644
673
  * - terminal → the work already concluded elsewhere; consume the message;
645
674
  * - unknown → not ours to run; terminate it.
646
675
  */