instar 1.3.28 → 1.3.30

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.
@@ -3811,21 +3811,46 @@ export function createRoutes(ctx) {
3811
3811
  // ledger is null (feature OFF, default). Reads serve toApiView ONLY —
3812
3812
  // detail.full NEVER crosses the boundary (§4.8). POST is the one mutating
3813
3813
  // route: requires X-Instar-Request intent + server-validated initiative.
3814
+ // ETag/304 for the Process Health tab's diff-aware polling (tab spec §3/§4.3).
3815
+ // Deterministic SHA over the fully-assembled body (incl. any rollout block);
3816
+ // rely on V8 insertion order — do NOT sort keys.
3817
+ const sendFailureJson = (req, res, body) => {
3818
+ const etag = `"${createHash('sha256').update(JSON.stringify(body)).digest('hex').slice(0, 16)}"`;
3819
+ res.setHeader('ETag', etag);
3820
+ if (req.headers['if-none-match'] === etag) {
3821
+ res.status(304).end();
3822
+ return;
3823
+ }
3824
+ res.json(body);
3825
+ };
3826
+ // Parse a ?before=<ISO-ts> upper-bound param. Returns 400 sentinel on NaN.
3827
+ const parseBeforeMs = (raw) => {
3828
+ if (typeof raw !== 'string')
3829
+ return { ok: true, ms: undefined };
3830
+ const ms = Date.parse(raw);
3831
+ return Number.isNaN(ms) ? { ok: false } : { ok: true, ms };
3832
+ };
3814
3833
  router.get('/failures', (req, res) => {
3815
3834
  if (!ctx.failureLedger) {
3816
3835
  res.status(503).json({ error: 'failure-learning disabled' });
3817
3836
  return;
3818
3837
  }
3819
3838
  const q = req.query;
3839
+ const before = parseBeforeMs(q.before);
3840
+ if (!before.ok) {
3841
+ res.status(400).json({ error: 'invalid before= (expected an ISO timestamp)' });
3842
+ return;
3843
+ }
3820
3844
  const records = ctx.failureLedger.list({
3821
3845
  source: typeof q.source === 'string' ? q.source : undefined,
3822
3846
  category: typeof q.category === 'string' ? q.category : undefined,
3823
3847
  initiativeId: typeof q.initiativeId === 'string' ? q.initiativeId : undefined,
3824
3848
  attribution: q.attribution === 'automatic' || q.attribution === 'one-tap' || q.attribution === 'inferred' ? q.attribution : undefined,
3825
3849
  status: typeof q.status === 'string' ? q.status : undefined,
3850
+ beforeMs: before.ms,
3826
3851
  limit: typeof q.limit === 'string' && /^\d+$/.test(q.limit) ? Number(q.limit) : undefined,
3827
3852
  });
3828
- res.json({ failures: records.map((r) => FailureLedger.toApiView(r)) });
3853
+ sendFailureJson(req, res, { failures: records.map((r) => FailureLedger.toApiView(r)) });
3829
3854
  });
3830
3855
  router.get('/failures/analysis', (req, res) => {
3831
3856
  if (!ctx.failureLedger) {
@@ -3834,15 +3859,34 @@ export function createRoutes(ctx) {
3834
3859
  }
3835
3860
  const sinceDays = typeof req.query.sinceDays === 'string' ? Number(req.query.sinceDays) : undefined;
3836
3861
  const sinceMs = sinceDays && sinceDays > 0 ? Date.now() - sinceDays * 86400_000 : undefined;
3837
- res.json(ctx.failureLedger.analyze({ sinceMs }));
3862
+ // rollout is assembled HERE (analyze() has no config access). Stage derives
3863
+ // from the two failureLearning booleans; the 4th "default-on" stage has no
3864
+ // per-agent flag and is never returned (tab renders it as a future step).
3865
+ const fl = ctx.config.monitoring?.failureLearning;
3866
+ const enabled = !!fl?.enabled;
3867
+ const insightTelegramEscalation = !!fl?.insightTelegramEscalation;
3868
+ const stage = !enabled ? 'dark' : insightTelegramEscalation ? 'insight-push' : 'capture-only';
3869
+ const rollout = { stage, enabled, insightTelegramEscalation };
3870
+ sendFailureJson(req, res, { ...ctx.failureLedger.analyze({ sinceMs }), rollout });
3838
3871
  });
3839
3872
  router.get('/failures/insights', (req, res) => {
3840
3873
  if (!ctx.failureLedger) {
3841
3874
  res.status(503).json({ error: 'failure-learning disabled' });
3842
3875
  return;
3843
3876
  }
3844
- const status = typeof req.query.status === 'string' ? req.query.status : undefined;
3845
- res.json({ insights: ctx.failureLedger.listInsights(status ? { status: status } : {}) });
3877
+ const q = req.query;
3878
+ const before = parseBeforeMs(q.before);
3879
+ if (!before.ok) {
3880
+ res.status(400).json({ error: 'invalid before= (expected an ISO timestamp)' });
3881
+ return;
3882
+ }
3883
+ const status = typeof q.status === 'string' ? q.status : undefined;
3884
+ const insights = ctx.failureLedger.listInsights({
3885
+ status: status ? status : undefined,
3886
+ beforeMs: before.ms,
3887
+ limit: typeof q.limit === 'string' && /^\d+$/.test(q.limit) ? Number(q.limit) : 50,
3888
+ });
3889
+ sendFailureJson(req, res, { insights });
3846
3890
  });
3847
3891
  // The analyzer + closed-loop tick (spec §4.4/§4.6.1). Invoked by the off-by-
3848
3892
  // default `failure-analyzer` builtin job (Tier-1 supervised). Discovers