@forwardimpact/libbridge 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libbridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Threaded-channel bridge primitives — relay messages between human channels (GitHub Discussions, Microsoft Teams) and the Kata agent team.",
5
5
  "keywords": [
6
6
  "bridge",
@@ -42,8 +42,8 @@
42
42
  "dependencies": {
43
43
  "@forwardimpact/libindex": "^0.1.38",
44
44
  "@forwardimpact/libstorage": "^0.1.78",
45
- "@hono/node-server": "^2.0.3",
46
- "hono": "^4.12.22"
45
+ "@hono/node-server": "^2.0.4",
46
+ "hono": "^4.12.23"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@forwardimpact/libharness": "^0.1.21"
@@ -48,7 +48,20 @@ export function validateCallbackPayload(body) {
48
48
  };
49
49
  }
50
50
 
51
- const ALLOWED_TRIGGER_KINDS = new Set(["responses", "elapsed", "any"]);
51
+ const ALLOWED_TRIGGER_KINDS = new Set([
52
+ "missing_input",
53
+ "escalation_needed",
54
+ "elapsed",
55
+ ]);
56
+
57
+ const TRIGGER_FIELD_VALIDATORS = {
58
+ replies: (raw) => {
59
+ const n = Number(raw);
60
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
61
+ },
62
+ elapsed: (raw) => (typeof raw === "string" ? raw : undefined),
63
+ signal: (raw) => (typeof raw === "string" && raw ? raw : undefined),
64
+ };
52
65
 
53
66
  /**
54
67
  * Validate and sanitize a trigger object at the payload boundary.
@@ -63,14 +76,11 @@ function validateTrigger(raw) {
63
76
  return undefined;
64
77
  }
65
78
  const trigger = { kind: raw.kind };
66
- if (raw.responses !== undefined) {
67
- const n = Number(raw.responses);
68
- if (!Number.isFinite(n) || n < 0) return undefined;
69
- trigger.responses = n;
70
- }
71
- if (raw.elapsed !== undefined) {
72
- if (typeof raw.elapsed !== "string") return undefined;
73
- trigger.elapsed = raw.elapsed;
79
+ for (const [field, validate] of Object.entries(TRIGGER_FIELD_VALIDATORS)) {
80
+ if (raw[field] === undefined) continue;
81
+ const clean = validate(raw[field]);
82
+ if (clean === undefined) return undefined;
83
+ trigger[field] = clean;
74
84
  }
75
85
  return trigger;
76
86
  }
@@ -37,15 +37,17 @@ export class ProgressTicker {
37
37
  throw new Error("tick must be a function");
38
38
  }
39
39
  this.stop(token);
40
- const timer = setInterval(async () => {
40
+ const safeTick = async () => {
41
41
  try {
42
42
  await tick();
43
43
  } catch {
44
44
  this.stop(token);
45
45
  }
46
- }, this.#intervalMs);
46
+ };
47
+ const timer = setInterval(safeTick, this.#intervalMs);
47
48
  timer.unref?.();
48
49
  this.#timers.set(token, timer);
50
+ safeTick();
49
51
  }
50
52
 
51
53
  /**
@@ -119,12 +119,10 @@ export class ResumeScheduler {
119
119
  opened_at: openedAt,
120
120
  history_index_at_open: ctx.history.length,
121
121
  };
122
- if (trigger.kind === "elapsed" || trigger.kind === "either") {
123
- if (typeof trigger.elapsed === "string") {
124
- const dueAt = openedAt + parseIsoDuration(trigger.elapsed);
125
- ctx.open_rfcs[correlationId].due_at = dueAt;
126
- this.#elapsed.schedule(correlationId, dueAt);
127
- }
122
+ if (trigger.kind === "elapsed" && typeof trigger.elapsed === "string") {
123
+ const dueAt = openedAt + parseIsoDuration(trigger.elapsed);
124
+ ctx.open_rfcs[correlationId].due_at = dueAt;
125
+ this.#elapsed.schedule(correlationId, dueAt);
128
126
  }
129
127
  }
130
128
 
@@ -204,7 +202,7 @@ export class ResumeScheduler {
204
202
  const trigger = rfc.trigger;
205
203
  if (!trigger) continue;
206
204
  const observed = {
207
- responses: ctx.history.length - rfc.history_index_at_open,
205
+ replies: ctx.history.length - rfc.history_index_at_open,
208
206
  opened_at: rfc.opened_at,
209
207
  };
210
208
  if (evaluateTrigger(trigger, observed, Date.now()).fired) {
package/src/triggers.js CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * @typedef {object} ResumeTrigger
3
- * @property {"responses"|"elapsed"|"either"} kind
4
- * @property {number} [responses] - Number of new responses needed.
5
- * For `kind: "either"`, optional alongside `elapsed`.
6
- * @property {string} [elapsed] - ISO-8601 duration, e.g. `"P14D"`, `"PT12H"`,
7
- * `"P1DT6H"`. Required for `kind: "elapsed"`; optional for `"either"`.
3
+ * @property {"missing_input"|"escalation_needed"|"elapsed"} kind
4
+ * @property {number} [replies] - Required for `kind: "missing_input"`.
5
+ * Number of new replies on the dispatching thread needed to fire.
6
+ * @property {string} [elapsed] - Required for `kind: "elapsed"`.
7
+ * ISO-8601 duration, e.g. `"P14D"`, `"PT12H"`, `"P1DT6H"`.
8
+ * @property {string} [signal] - Required for `kind: "escalation_needed"`.
9
+ * Reserved for future use. The bridge throws when evaluating this kind
10
+ * until signal-based resume support lands.
8
11
  */
9
12
 
10
13
  const ISO_8601_DURATION =
@@ -42,7 +45,7 @@ export function parseIsoDuration(duration) {
42
45
  * Evaluate whether a resume trigger has fired.
43
46
  *
44
47
  * @param {ResumeTrigger} trigger
45
- * @param {{responses?: number, opened_at?: number}} observed
48
+ * @param {{replies?: number, opened_at?: number}} observed
46
49
  * @param {number} now - ms epoch (caller-provided for testability)
47
50
  * @returns {{fired: boolean, due_at?: number}}
48
51
  */
@@ -56,37 +59,27 @@ export function evaluateTrigger(trigger, observed, now) {
56
59
  observed ??= {};
57
60
 
58
61
  switch (trigger.kind) {
59
- case "responses":
60
- return evaluateResponses(trigger, observed);
62
+ case "missing_input":
63
+ return evaluateMissingInput(trigger, observed);
61
64
  case "elapsed":
62
65
  return evaluateElapsed(trigger, observed, now);
63
- case "either": {
64
- const r =
65
- trigger.responses !== undefined
66
- ? evaluateResponses(trigger, observed)
67
- : { fired: false };
68
- const e =
69
- trigger.elapsed !== undefined
70
- ? evaluateElapsed(trigger, observed, now)
71
- : { fired: false };
72
- if (r.fired || e.fired) return { fired: true };
73
- return e.due_at !== undefined
74
- ? { fired: false, due_at: e.due_at }
75
- : { fired: false };
76
- }
66
+ case "escalation_needed":
67
+ throw new Error(
68
+ "escalation_needed is reserved for future use. See the follow-up spec for signal-based resume.",
69
+ );
77
70
  default:
78
71
  throw new Error(`Unsupported trigger kind: ${trigger.kind}`);
79
72
  }
80
73
  }
81
74
 
82
- function evaluateResponses(trigger, observed) {
83
- if (typeof trigger.responses !== "number" || trigger.responses < 1) {
75
+ function evaluateMissingInput(trigger, observed) {
76
+ if (typeof trigger.replies !== "number" || trigger.replies < 1) {
84
77
  throw new Error(
85
- 'trigger.responses must be a positive number for kind "responses"',
78
+ 'trigger.replies must be a positive number for kind "missing_input"',
86
79
  );
87
80
  }
88
- const seen = observed.responses ?? 0;
89
- return { fired: seen >= trigger.responses };
81
+ const seen = observed.replies ?? 0;
82
+ return { fired: seen >= trigger.replies };
90
83
  }
91
84
 
92
85
  function evaluateElapsed(trigger, observed, now) {