@senpi/runtime 3.0.111 → 3.0.112

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 (72) hide show
  1. package/dist/cli/guide-commands.d.ts.map +1 -1
  2. package/dist/cli/guide-commands.js +11 -0
  3. package/dist/cli/guide-commands.js.map +1 -1
  4. package/dist/cli/senpi-commands.d.ts.map +1 -1
  5. package/dist/cli/senpi-commands.js +70 -0
  6. package/dist/cli/senpi-commands.js.map +1 -1
  7. package/dist/index.d.ts +11 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +61 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/runtime/auto-update/coordinator.d.ts +15 -1
  12. package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
  13. package/dist/runtime/auto-update/coordinator.js +53 -0
  14. package/dist/runtime/auto-update/coordinator.js.map +1 -1
  15. package/dist/runtime/auto-update/types.d.ts +14 -0
  16. package/dist/runtime/auto-update/types.d.ts.map +1 -1
  17. package/dist/runtime/auto-update/types.js.map +1 -1
  18. package/dist/runtime-api/intake/process-error-post.d.ts +6 -1
  19. package/dist/runtime-api/intake/process-error-post.d.ts.map +1 -1
  20. package/dist/runtime-api/intake/process-error-post.js +5 -1
  21. package/dist/runtime-api/intake/process-error-post.js.map +1 -1
  22. package/dist/runtime-api/intake/process-signal-post.d.ts +7 -1
  23. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  24. package/dist/runtime-api/intake/process-signal-post.js +5 -0
  25. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  26. package/dist/runtime-api/intake/scaffold-tick-router.d.ts +39 -0
  27. package/dist/runtime-api/intake/scaffold-tick-router.d.ts.map +1 -0
  28. package/dist/runtime-api/intake/scaffold-tick-router.js +122 -0
  29. package/dist/runtime-api/intake/scaffold-tick-router.js.map +1 -0
  30. package/dist/runtime-api/intake/types.d.ts +49 -0
  31. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  32. package/dist/runtime-api/routes/signals.d.ts +2 -0
  33. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  34. package/dist/runtime-api/routes/signals.js +3 -0
  35. package/dist/runtime-api/routes/signals.js.map +1 -1
  36. package/dist/runtime-api/routes/signals.schema.d.ts +42 -0
  37. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  38. package/dist/runtime-api/routes/signals.schema.js +37 -0
  39. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  40. package/dist/runtime-api/server.d.ts +7 -1
  41. package/dist/runtime-api/server.d.ts.map +1 -1
  42. package/dist/runtime-api/server.js +1 -0
  43. package/dist/runtime-api/server.js.map +1 -1
  44. package/dist/scanners/external/scaffold/delivery.py +64 -17
  45. package/dist/scanners/external/scaffold/scaffold.py +128 -40
  46. package/dist/scanners/external/scaffold/senpi_mcp.py +88 -8
  47. package/dist/scanners/external/scaffold/tick_facts.py +130 -0
  48. package/dist/scanners/runtime-module.d.ts.map +1 -1
  49. package/dist/scanners/runtime-module.js +8 -1
  50. package/dist/scanners/runtime-module.js.map +1 -1
  51. package/dist/skills-manager/manifest.d.ts.map +1 -1
  52. package/dist/skills-manager/manifest.js +1 -0
  53. package/dist/skills-manager/manifest.js.map +1 -1
  54. package/dist/skills-manager/skills-manifest.json +1 -0
  55. package/dist/telemetry/live-telemetry.d.ts +54 -8
  56. package/dist/telemetry/live-telemetry.d.ts.map +1 -1
  57. package/dist/telemetry/live-telemetry.js +177 -7
  58. package/dist/telemetry/live-telemetry.js.map +1 -1
  59. package/dist/telemetry/scanner-tick-feed.d.ts +87 -0
  60. package/dist/telemetry/scanner-tick-feed.d.ts.map +1 -0
  61. package/dist/telemetry/scanner-tick-feed.js +48 -0
  62. package/dist/telemetry/scanner-tick-feed.js.map +1 -0
  63. package/dist/telemetry/tick-frame.d.ts +197 -0
  64. package/dist/telemetry/tick-frame.d.ts.map +1 -0
  65. package/dist/telemetry/tick-frame.js +347 -0
  66. package/dist/telemetry/tick-frame.js.map +1 -0
  67. package/dist/utils/telemetry-required-log.d.ts +13 -0
  68. package/dist/utils/telemetry-required-log.d.ts.map +1 -1
  69. package/dist/utils/telemetry-required-log.js +5 -1
  70. package/dist/utils/telemetry-required-log.js.map +1 -1
  71. package/openclaw.plugin.json +1 -1
  72. package/package.json +1 -1
@@ -23,10 +23,52 @@ export declare const SignalsBodySchema: import("@sinclair/typebox").TObject<{
23
23
  marginPct: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
24
24
  leverage: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
25
25
  }>>;
26
+ tick: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
27
+ tick_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
28
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
29
+ candidate_count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
30
+ duration_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
31
+ mcp_failures: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
32
+ mcp_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
33
+ mcp_calls: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
34
+ tool: import("@sinclair/typebox").TString;
35
+ ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
36
+ args: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
37
+ error_type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
38
+ }>>>;
39
+ quiet_ticks: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
40
+ lag_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
41
+ journal_pending: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
42
+ commit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
43
+ state: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
44
+ state_fields: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
45
+ state_changed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
46
+ }>>;
26
47
  }>;
27
48
  /** TypeBox schema for the /errors request body. */
28
49
  export declare const ErrorsBodySchema: import("@sinclair/typebox").TObject<{
29
50
  scanner_id: import("@sinclair/typebox").TString;
30
51
  error: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>;
52
+ tick: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
53
+ tick_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
54
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
55
+ candidate_count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
56
+ duration_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
57
+ mcp_failures: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
58
+ mcp_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
59
+ mcp_calls: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
60
+ tool: import("@sinclair/typebox").TString;
61
+ ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
62
+ args: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
63
+ error_type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
64
+ }>>>;
65
+ quiet_ticks: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
66
+ lag_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
67
+ journal_pending: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
68
+ commit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
69
+ state: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
70
+ state_fields: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
71
+ state_changed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
72
+ }>>;
31
73
  }>;
32
74
  //# sourceMappingURL=signals.schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"signals.schema.d.ts","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAqCH,yDAAyD;AACzD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;EAM7B,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,gBAAgB;;;EAM5B,CAAC"}
1
+ {"version":3,"file":"signals.schema.d.ts","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgFH,yDAAyD;AACzD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;EAO5B,CAAC"}
@@ -38,14 +38,51 @@ const IntakeSignalSchema = Type.Object({
38
38
  // No max on leverage — it is clamped downstream by the exchange.
39
39
  leverage: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
40
40
  }, { additionalProperties: false });
41
+ /**
42
+ * One MCP call inside a tick's facts. `args` is an open record like a signal's `data` is: it is
43
+ * author-supplied and cannot be enumerated here, and it is scrubbed and capped before it reaches
44
+ * any wire a browser reads.
45
+ */
46
+ const IntakeTickCallSchema = Type.Object({
47
+ tool: Type.String({ minLength: 1 }),
48
+ ms: Type.Optional(Type.Number()),
49
+ args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
50
+ error_type: Type.Optional(Type.String()),
51
+ }, { additionalProperties: false });
52
+ /**
53
+ * A tick's observability facts. Every field optional: a producer that sends none behaves exactly as
54
+ * before, and a producer that cannot measure one leaves it off rather than sending a zero, which
55
+ * would be read as a measurement.
56
+ *
57
+ * `additionalProperties: false` like the rest of the body — a key this box does not know is a
58
+ * producer/runtime mismatch worth a loud reject, not a silent drop.
59
+ */
60
+ const IntakeTickSchema = Type.Object({
61
+ tick_id: Type.Optional(Type.String({ minLength: 1 })),
62
+ status: Type.Optional(Type.String()),
63
+ candidate_count: Type.Optional(Type.Number()),
64
+ duration_ms: Type.Optional(Type.Number()),
65
+ mcp_failures: Type.Optional(Type.Number()),
66
+ mcp_ms: Type.Optional(Type.Number()),
67
+ mcp_calls: Type.Optional(Type.Array(IntakeTickCallSchema)),
68
+ quiet_ticks: Type.Optional(Type.Number()),
69
+ lag_ms: Type.Optional(Type.Number()),
70
+ journal_pending: Type.Optional(Type.Number()),
71
+ commit: Type.Optional(Type.String()),
72
+ state: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
73
+ state_fields: Type.Optional(Type.Number()),
74
+ state_changed: Type.Optional(Type.Number()),
75
+ }, { additionalProperties: false });
41
76
  /** TypeBox schema for the full /signals request body. */
42
77
  export const SignalsBodySchema = Type.Object({
43
78
  scanner_id: Type.String({ minLength: 1 }),
44
79
  signals: Type.Array(IntakeSignalSchema),
80
+ tick: Type.Optional(IntakeTickSchema),
45
81
  }, { additionalProperties: false });
46
82
  /** TypeBox schema for the /errors request body. */
47
83
  export const ErrorsBodySchema = Type.Object({
48
84
  scanner_id: Type.String({ minLength: 1 }),
49
85
  error: Type.Record(Type.String(), Type.Unknown()),
86
+ tick: Type.Optional(IntakeTickSchema),
50
87
  }, { additionalProperties: false });
51
88
  //# sourceMappingURL=signals.schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"signals.schema.js","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,iEAAiE;AACjE,MAAM,kBAAkB,GAAG,IAAI,CAAC,MAAM,CACpC;IACE,6EAA6E;IAC7E,8EAA8E;IAC9E,+EAA+E;IAC/E,sDAAsD;IACtD,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACxC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACpC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACpE,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE;IAC1B,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE;IAC1B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAChD,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IACzC,iFAAiF;IACjF,iFAAiF;IACjF,kFAAkF;IAClF,mFAAmF;IACnF,6CAA6C;IAC7C,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IACrD,+EAA+E;IAC/E,iFAAiF;IACjF,0EAA0E;IAC1E,iBAAiB;IACjB,gFAAgF;IAChF,oFAAoF;IACpF,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAC5E,iEAAiE;IACjE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC;CAC9D,EACD,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAChC,CAAC;AAEF,yDAAyD;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,MAAM,CAC1C;IACE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACzC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC;CACxC,EACD,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAChC,CAAC;AAEF,mDAAmD;AACnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CACzC;IACE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACzC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;CAClD,EACD,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAChC,CAAC"}
1
+ {"version":3,"file":"signals.schema.js","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"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"}
@@ -19,7 +19,7 @@ import type { RuntimeHandle } from "../runtime/run.js";
19
19
  import type { Logger } from "../utils/logger.js";
20
20
  import type { ApiConfig } from "./config.js";
21
21
  import type { IntakeContext } from "./intake/intake-context.js";
22
- import type { IntakeSignal } from "./intake/types.js";
22
+ import type { IntakeSignal, IntakeTickFacts } from "./intake/types.js";
23
23
  /**
24
24
  * Dependencies passed to {@link createApi}.
25
25
  */
@@ -53,6 +53,12 @@ export interface CreateApiDeps {
53
53
  * a no-op (which is what made the `/errors` boundary silent before B1 Task 2).
54
54
  */
55
55
  onError?: (scannerId: string, error: Record<string, unknown>) => void;
56
+ /**
57
+ * Called once per producer tick, from whichever of the two intake routes that tick took, with
58
+ * the facts the producer measured about the run itself. Feeds the live per-tick level and
59
+ * nothing else. Only used when `intakeContext` is present; defaults to a no-op.
60
+ */
61
+ onTick?: (scannerId: string, tick: IntakeTickFacts) => void;
56
62
  }
57
63
  /**
58
64
  * Artifacts returned by {@link createApi}.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAKxD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC3C,4EAA4E;IAC5E,SAAS,EAAE,SAAS,CAAC;IACrB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtF;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACvE;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAsF3D"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAKxD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIvE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC3C,4EAA4E;IAC5E,SAAS,EAAE,SAAS,CAAC;IACrB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtF;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;CAC7D;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAuF3D"}
@@ -100,6 +100,7 @@ export function createApi(deps) {
100
100
  updateLiveness: (id, now) => deps.intakeContext.liveness.touch(id, now),
101
101
  onAccepted: deps.onAccepted ?? (() => { }),
102
102
  onError: deps.onError ?? (() => { }),
103
+ onTick: deps.onTick ?? (() => { }),
103
104
  maxItemsPerSignalsRequest: deps.apiConfig.maxItemsPerSignalsRequest,
104
105
  logger: deps.logger,
105
106
  });
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"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"}
@@ -11,8 +11,10 @@ build_delivery_sink(
11
11
  clock: Callable[[], float] = time.time,
12
12
  ) -> sink
13
13
 
14
- The returned sink is a callable: sink(candidates, tick_id=None)
15
- On each call it:
14
+ The returned sink is a callable: sink(candidates, tick_id=None, tick=None)
15
+ `tick` is the scaffold's per-tick observability facts; the sink stamps the journal's
16
+ pending count onto them (it owns the journal) and rides them on the POST it already
17
+ makes. On each call it:
16
18
  1. Replays journal on first call (if file-backed), re-queuing valid undelivered entries
17
19
  2. Flushes still-valid undelivered entries from prior ticks (retry)
18
20
  3. Wraps this tick's candidates into wire signals (skips invalid ones), stamping the
@@ -22,8 +24,10 @@ On each call it:
22
24
  6. Interprets the response: accepted/expired/superseded/rejected = delivered;
23
25
  transport failure or non-success envelope leaves entry queued for retry
24
26
 
25
- sink.post_error(error: dict) -> None
26
- POSTs {scanner_id, error} to <intake_url>/errors. Fire-and-forget.
27
+ sink.post_error(error: dict, tick: dict | None = None) -> None
28
+ POSTs {scanner_id, error} to <intake_url>/errors. Fire-and-forget. A failed tick's
29
+ facts ride here for the same reason they ride /signals on a clean one: this is the
30
+ POST that tick already makes.
27
31
 
28
32
  Module constants:
29
33
  SIGNALS_PATH = "/signals"
@@ -76,6 +80,10 @@ def build_delivery_sink(
76
80
  # is not written again. Never cleared: an intake that refuses the field once would refuse it
77
81
  # again, so re-arming would buy back the guaranteed-400 round-trip on every later tick.
78
82
  intake_predates_tick_id = [False]
83
+ # The same latch for the body-level `tick` facts. Separate from the one above because
84
+ # they are separate fields on separate levels of the body: an intake may know one and
85
+ # not the other, and stripping both on either refusal would throw away what still works.
86
+ intake_predates_tick_facts = [False]
79
87
 
80
88
  # ------------------------------------------------------------------
81
89
  # Internal helpers
@@ -221,9 +229,23 @@ def build_delivery_sink(
221
229
  for sig in signals
222
230
  ]
223
231
 
224
- def _post_signals(signals: list[dict]) -> tuple[int, Any]:
225
- """POST the signals list to the intake."""
232
+ def _with_pending(tick: dict | None) -> dict | None:
233
+ """The tick facts plus the journal's BACKLOG, which only this closure knows.
234
+
235
+ Counted before this tick's own candidates are journalled, so the number is
236
+ signals from EARLIER ticks that the intake never acknowledged — a scanner
237
+ producing work that is not arriving. Counting this tick's own signals too
238
+ would put a 1 on every healthy signal-bearing tick and say nothing.
239
+ """
240
+ if tick is None:
241
+ return None
242
+ return {**tick, "journal_pending": len(journal.pending())}
243
+
244
+ def _post_signals(signals: list[dict], tick: dict | None = None) -> tuple[int, Any]:
245
+ """POST the signals list to the intake, with this tick's facts when it has any."""
226
246
  body = {"scanner_id": scanner_id, "signals": signals}
247
+ if tick is not None and not intake_predates_tick_facts[0]:
248
+ body["tick"] = tick
227
249
  # Log the exact wire shape so a 400 can be matched to the field that broke.
228
250
  if signals:
229
251
  log_event(
@@ -315,7 +337,7 @@ def build_delivery_sink(
315
337
  # The sink callable
316
338
  # ------------------------------------------------------------------
317
339
 
318
- def _sink(candidates, tick_id=None) -> None:
340
+ def _sink(candidates, tick_id=None, tick=None) -> None:
319
341
  tick_id = _usable_tick_id(tick_id)
320
342
  current_tick_id[0] = tick_id
321
343
 
@@ -330,6 +352,10 @@ def build_delivery_sink(
330
352
  # 1. Flush prior pending (retry candidates from older ticks)
331
353
  prior_entries = _flush_prior_pending(now_ms)
332
354
 
355
+ # Read here and not below: the backlog is what EARLIER ticks left behind, and
356
+ # step 2 is about to journal this tick's own candidates on top of it.
357
+ tick_body = _with_pending(tick)
358
+
333
359
  # 2. Wrap new candidates into signals
334
360
  new_signal_pairs = _wrap_candidates(candidates, produced_at_ms, tick_id)
335
361
 
@@ -351,9 +377,10 @@ def build_delivery_sink(
351
377
  all_signals.append(sig)
352
378
  all_eids.append(eid)
353
379
 
354
- # 3. POST (always, even when signals==[])
380
+ # 3. POST (always, even when signals==[]) — which is what makes this the channel
381
+ # the tick facts ride: a quiet scanner still posts, every tick.
355
382
  try:
356
- status, parsed = _post_signals(all_signals)
383
+ status, parsed = _post_signals(all_signals, tick_body)
357
384
  except TransportError as exc:
358
385
  log_event("delivery_transport_error", error=str(exc))
359
386
  # All signals remain undelivered -> retry on next tick
@@ -367,12 +394,15 @@ def build_delivery_sink(
367
394
  # the same batch without the field so one unknown key cannot strand real signals, and
368
395
  # latch the discovery so later ticks leave the field off and stop paying the refused POST.
369
396
  # Entries already journalled with an id still reach here and are still stripped per POST.
370
- if _is_shape_reject(status, parsed) and any(
371
- isinstance(sig, dict) and "tick_id" in sig for sig in all_signals
372
- ):
373
- if not intake_predates_tick_id[0]:
397
+ sent_tick_id = any(isinstance(sig, dict) and "tick_id" in sig for sig in all_signals)
398
+ sent_tick_facts = tick_body is not None and not intake_predates_tick_facts[0]
399
+ if _is_shape_reject(status, parsed) and (sent_tick_id or sent_tick_facts):
400
+ if sent_tick_id and not intake_predates_tick_id[0]:
374
401
  intake_predates_tick_id[0] = True
375
402
  log_event("delivery_intake_predates_tick_id", scanner_id=scanner_id)
403
+ if sent_tick_facts:
404
+ intake_predates_tick_facts[0] = True
405
+ log_event("delivery_intake_predates_tick_facts", scanner_id=scanner_id)
376
406
  try:
377
407
  status, parsed = _post_signals(_without_tick_id(all_signals))
378
408
  except TransportError as exc:
@@ -386,15 +416,32 @@ def build_delivery_sink(
386
416
  # even when the retry above had to send them without it.
387
417
  _interpret_response(status, parsed, all_eids, all_signals)
388
418
 
389
- def _post_error(error: dict) -> None:
390
- """POST an error dict. Fire-and-forget."""
391
- body = {"scanner_id": scanner_id, "error": error}
419
+ def _post_error(error: dict, tick: dict | None = None) -> None:
420
+ """POST an error dict, with the failed tick's facts when the caller has them.
421
+
422
+ Fire-and-forget, with one exception: an intake that refuses the body because it
423
+ does not know `tick` is retried once without it and latched, so a failed tick's
424
+ error record is never lost to a field that only telemetry wanted.
425
+ """
426
+ tick_body = _with_pending(tick)
392
427
  url = intake_url.rstrip("/") + ERRORS_PATH
393
428
  # The error belongs to exactly one tick and already names it; a body that
394
429
  # names none leaves the key off rather than borrowing the current one.
395
430
  origin = _tick_field(error.get("tick_id")) if isinstance(error, dict) else {}
431
+
432
+ def _body(with_tick: bool) -> dict:
433
+ body = {"scanner_id": scanner_id, "error": error}
434
+ if with_tick and tick_body is not None and not intake_predates_tick_facts[0]:
435
+ body["tick"] = tick_body
436
+ return body
437
+
396
438
  try:
397
- post_json(url, body, timeout)
439
+ sent_tick_facts = tick_body is not None and not intake_predates_tick_facts[0]
440
+ status, parsed = post_json(url, _body(True), timeout)
441
+ if sent_tick_facts and _is_shape_reject(status, parsed):
442
+ intake_predates_tick_facts[0] = True
443
+ log_event("delivery_intake_predates_tick_facts", scanner_id=scanner_id)
444
+ post_json(url, _body(False), timeout)
398
445
  except TransportError as exc:
399
446
  log_event("delivery_error_post_failed", error=str(exc), **origin)
400
447
  except Exception as exc: # noqa: BLE001
@@ -43,6 +43,7 @@ from .loop_primitives import (
43
43
  log_event,
44
44
  )
45
45
  from .state_manager import StateManager
46
+ from .tick_facts import build_tick_facts, count_changed, project_state_row
46
47
  from .ulid import new_ulid
47
48
 
48
49
 
@@ -105,22 +106,22 @@ def _build_senpi_mcp() -> Any:
105
106
  )
106
107
 
107
108
 
108
- def _sink_accepts_tick_id(sink: Any) -> bool:
109
- """Whether `sink` takes the tick id as a keyword.
109
+ def _accepts_kwarg(fn: Any, name: str) -> bool:
110
+ """Whether `fn` takes `name` as a keyword argument.
110
111
 
111
112
  Read from the signature, once, rather than calling with the keyword and retrying on
112
113
  TypeError: a sink that raises TypeError from its own body would then be called twice and
113
- deliver the same candidates twice. A sink whose signature cannot be read keeps the
114
- one-argument call it has today.
114
+ deliver the same candidates twice. A callable whose signature cannot be read keeps the
115
+ narrower call it has today.
115
116
  """
116
117
  try:
117
- params = list(inspect.signature(sink).parameters.values())
118
+ params = list(inspect.signature(fn).parameters.values())
118
119
  except (TypeError, ValueError):
119
120
  return False
120
121
  return any(
121
122
  p.kind == inspect.Parameter.VAR_KEYWORD
122
123
  or (
123
- p.name == "tick_id"
124
+ p.name == name
124
125
  and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
125
126
  )
126
127
  for p in params
@@ -260,7 +261,24 @@ def run_scaffold(
260
261
  # The MCP boundary's per-tick failure tally, when the client exposes one. Duck-typed:
261
262
  # an injected stub without it (a validation run) simply never reclassifies a tick.
262
263
  mcp_tally = getattr(senpi_mcp, "tick_errors", None)
263
- sink_takes_tick_id = sink is not None and _sink_accepts_tick_id(sink)
264
+ sink_takes_tick_id = sink is not None and _accepts_kwarg(sink, "tick_id")
265
+ # The per-tick observability facts ride the POST the sink already makes. A sink that
266
+ # predates them is called exactly as before, so an older sink is not broken by them.
267
+ sink_takes_tick = sink is not None and _accepts_kwarg(sink, "tick")
268
+ post_error_takes_tick = (
269
+ sink is not None
270
+ and hasattr(sink, "post_error")
271
+ and _accepts_kwarg(sink.post_error, "tick")
272
+ )
273
+
274
+ # Consecutive ticks that produced no candidate. 47 quiet ticks is information; the word
275
+ # "quiet" repeated 47 times is not, so the streak is counted here and sent as a number.
276
+ quiet_ticks = 0
277
+ # The previous tick's start, for the cadence lag. None on the first tick of a process:
278
+ # there is no gap to measure yet, and a 0 would claim the scanner was exactly on time.
279
+ prev_tick_started_at: Optional[float] = None
280
+ # The last projection reported, so `state_changed` says what MOVED rather than what exists.
281
+ prev_state_projection: Optional[dict] = None
264
282
 
265
283
  try:
266
284
  while not stop_event.is_set():
@@ -275,6 +293,14 @@ def run_scaffold(
275
293
  # error it reports names the same tick.
276
294
  tick_id = new_ulid()
277
295
  tick_started_at = time.time()
296
+ # Actual gap between tick starts, minus the cadence the scanner is configured for.
297
+ # A positive number is a scanner falling behind its own interval.
298
+ lag_ms = (
299
+ None
300
+ if prev_tick_started_at is None
301
+ else int(round((tick_started_at - prev_tick_started_at - interval) * 1000))
302
+ )
303
+ prev_tick_started_at = tick_started_at
278
304
  tick_status = "ok"
279
305
  tick_error = None
280
306
  candidates: list = []
@@ -365,7 +391,12 @@ def run_scaffold(
365
391
  error_count=mcp_error_count,
366
392
  )
367
393
 
394
+ # The quiet streak counts ticks, not words: a tick that produced a
395
+ # candidate ends the streak whatever its status.
396
+ quiet_ticks = 0 if candidates else quiet_ticks + 1
397
+
368
398
  # --- Clean tick: commit ctx.state, deliver candidates ---
399
+ persist_error = None
369
400
  if tick_status == "ok":
370
401
  # Transactional commit. A failed persist RAISES (StateManager
371
402
  # enforces it) and does NOT advance the committed snapshot — so
@@ -385,28 +416,75 @@ def run_scaffold(
385
416
  error_type=type(commit_exc).__name__,
386
417
  error=str(commit_exc),
387
418
  )
388
- if sink is not None and hasattr(sink, "post_error"):
389
- _post_error_safe(
390
- sink,
391
- {
392
- "type": "state_persist_failed",
393
- "tick": tick_count,
394
- "tick_id": tick_id,
395
- "message": (
396
- "scanner state could not be persisted; "
397
- "tick discarded, state not advanced"
398
- ),
399
- },
400
- tick_count,
401
- tick_id,
402
- )
419
+ persist_error = {
420
+ "type": "state_persist_failed",
421
+ "tick": tick_count,
422
+ "tick_id": tick_id,
423
+ "message": (
424
+ "scanner state could not be persisted; "
425
+ "tick discarded, state not advanced"
426
+ ),
427
+ }
428
+ commit_outcome = "committed" if committed else "persist_failed"
429
+ else:
430
+ # Non-ok tick (error / timeout / malformed): roll back the
431
+ # working buffer (discard the tick's mutations — state not
432
+ # advanced) and report to /errors via the sink, when supported.
433
+ ctx.state.rollback()
434
+ committed = False
435
+ commit_outcome = "rolled_back"
436
+
437
+ # Read AFTER the commit/rollback above, so this is always the last
438
+ # COMMITTED row — stale by one tick on a failed tick, which is exactly
439
+ # when the last state the scanner agreed on is worth seeing.
440
+ state_row = ctx.state.last()
441
+ state_projection = project_state_row(state_row)
442
+ state_fields = len(state_row) if isinstance(state_row, dict) else None
443
+ state_changed = (
444
+ None
445
+ if state_fields is None
446
+ else count_changed(state_projection, prev_state_projection)
447
+ )
448
+ prev_state_projection = state_projection if state_fields is not None else None
449
+
450
+ tick_facts = build_tick_facts(
451
+ tick_id=tick_id,
452
+ status=tick_status,
453
+ candidate_count=len(candidates),
454
+ duration_ms=duration_ms,
455
+ commit=commit_outcome,
456
+ quiet_ticks=quiet_ticks,
457
+ lag_ms=lag_ms,
458
+ mcp_snapshot=mcp_tally.snapshot() if mcp_tally is not None else None,
459
+ call_snapshot=(
460
+ mcp_tally.call_snapshot()
461
+ if mcp_tally is not None and hasattr(mcp_tally, "call_snapshot")
462
+ else None
463
+ ),
464
+ state_row=state_row,
465
+ state_fields=state_fields,
466
+ state_changed=state_changed,
467
+ )
468
+
469
+ # Exactly one POST carries this tick's facts: /signals on a clean tick,
470
+ # /errors on any tick that failed. Both already happen every tick, so the
471
+ # facts add no request of their own.
472
+ sink_can_post_error = sink is not None and hasattr(sink, "post_error")
473
+ facts_for_error = tick_facts if post_error_takes_tick else None
474
+ if tick_status == "ok":
475
+ if persist_error is not None and sink_can_post_error:
476
+ _post_error_safe(
477
+ sink, persist_error, tick_count, tick_id, facts_for_error
478
+ )
403
479
 
404
480
  if committed and sink is not None:
405
481
  try:
482
+ kwargs = {}
406
483
  if sink_takes_tick_id:
407
- sink(candidates, tick_id=tick_id)
408
- else:
409
- sink(candidates)
484
+ kwargs["tick_id"] = tick_id
485
+ if sink_takes_tick:
486
+ kwargs["tick"] = tick_facts
487
+ sink(candidates, **kwargs)
410
488
  except Exception as sink_exc: # noqa: BLE001
411
489
  log_event(
412
490
  "scaffold_sink_error",
@@ -415,18 +493,14 @@ def run_scaffold(
415
493
  error_type=type(sink_exc).__name__,
416
494
  error=str(sink_exc),
417
495
  )
418
- else:
419
- # Non-ok tick (error / timeout / malformed): roll back the
420
- # working buffer (discard the tick's mutations — state not
421
- # advanced) and report to /errors via the sink, when supported.
422
- ctx.state.rollback()
423
- if sink is not None and hasattr(sink, "post_error"):
424
- _post_error_safe(
425
- sink,
426
- tick_error or {"type": tick_status, "tick_id": tick_id},
427
- tick_count,
428
- tick_id,
429
- )
496
+ elif sink_can_post_error:
497
+ _post_error_safe(
498
+ sink,
499
+ tick_error or {"type": tick_status, "tick_id": tick_id},
500
+ tick_count,
501
+ tick_id,
502
+ facts_for_error,
503
+ )
430
504
 
431
505
  log_event(
432
506
  "scaffold_tick_finished",
@@ -506,10 +580,24 @@ def _classify_mcp_errors(mcp_tally: Any, candidates: list) -> tuple[int, Optiona
506
580
  }
507
581
 
508
582
 
509
- def _post_error_safe(sink: Any, error: dict, tick_count: int, tick_id: str) -> None:
510
- """Call sink.post_error(error), swallowing/logging any failure."""
583
+ def _post_error_safe(
584
+ sink: Any,
585
+ error: dict,
586
+ tick_count: int,
587
+ tick_id: str,
588
+ tick: Optional[dict] = None,
589
+ ) -> None:
590
+ """Call sink.post_error(error), swallowing/logging any failure.
591
+
592
+ `tick` is passed only when the sink's signature accepts it — the caller checks
593
+ once at launch, so a sink that predates the per-tick facts keeps its one-argument
594
+ call rather than being handed a keyword it would raise on.
595
+ """
511
596
  try:
512
- sink.post_error(error)
597
+ if tick is None:
598
+ sink.post_error(error)
599
+ else:
600
+ sink.post_error(error, tick=tick)
513
601
  except Exception as perr: # noqa: BLE001
514
602
  log_event(
515
603
  "scaffold_post_error_failed",